From 6272e3549ea31cd876f5488f2485ee733c14e2e7 Mon Sep 17 00:00:00 2001
From: Maz Jaleel <mazjaleel@gmail.com>
Date: Fri, 18 Dec 2015 15:36:34 -0600
Subject: [PATCH] support checking if app installed, closes #10 Simplify API
 for opening apps, as well as supporting more apps.

---
 Appz/Appz.xcodeproj/project.pbxproj                |  4 +-
 Appz/Appz/Apps/AppStore.swift                      | 10 ++---
 Appz/Appz/Apps/Instagram.swift                     | 10 ++---
 Appz/Appz/Apps/Mail.swift                          | 10 ++---
 Appz/Appz/Apps/Messages.swift                      | 10 ++---
 Appz/Appz/Apps/Settings.swift                      | 10 ++---
 Appz/Appz/Apps/Twitter.swift                       | 10 ++---
 Appz/Appz/Entity/ApplicationCaller.swift           | 15 ++++---
 Appz/Appz/Entity/ExternalApplication.swift         |  4 +-
 Appz/AppzTests/AppsTests/SettingsTests.swift       |  2 +-
 .../EntityTests/ApplicationCallerTests.swift       | 14 ++++---
 Playground/Playground.playground/Contents.swift    | 40 ++++++++----------
 .../Playground.playground/contents.xcplayground    |  2 +-
 README.md                                          | 48 ++++++++++------------
 14 files changed, 94 insertions(+), 95 deletions(-)

diff --git a/Appz/Appz.xcodeproj/project.pbxproj b/Appz/Appz.xcodeproj/project.pbxproj
index 9a9ec8d..cc52408 100644
--- a/Appz/Appz.xcodeproj/project.pbxproj
+++ b/Appz/Appz.xcodeproj/project.pbxproj
@@ -150,9 +150,9 @@
 		826ACF171BF1BCF500B5FC5D /* Entity */ = {
 			isa = PBXGroup;
 			children = (
+				826ACF5B1BF1E26B00B5FC5D /* ApplicationCaller.swift */,
 				826ACF181BF1BD1F00B5FC5D /* Applications.swift */,
 				826ACF1A1BF1BD7F00B5FC5D /* ExternalApplication.swift */,
-				826ACF5B1BF1E26B00B5FC5D /* ApplicationCaller.swift */,
 				82D9A0FA1C1E495D00EAC0ED /* Path.swift */,
 			);
 			path = Entity;
@@ -174,8 +174,8 @@
 		826ACF231BF1BFE100B5FC5D /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
-				826ACF241BF1C00800B5FC5D /* UIApplication+Appz.swift */,
 				826ACF5D1BF1E4BE00B5FC5D /* NSExtensionContext+Appz.swift */,
+				826ACF241BF1C00800B5FC5D /* UIApplication+Appz.swift */,
 			);
 			path = Extensions;
 			sourceTree = "<group>";
diff --git a/Appz/Appz/Apps/AppStore.swift b/Appz/Appz/Apps/AppStore.swift
index 36476fd..37a0d1b 100644
--- a/Appz/Appz/Apps/AppStore.swift
+++ b/Appz/Appz/Apps/AppStore.swift
@@ -13,12 +13,12 @@ public extension Applications {
     
     public struct AppStore: ExternalApplication {
         
+        public typealias ActionType = Applications.AppStore.Action
+
         public let scheme = "itms-apps:"
-        public let fallbackURL: String? = "http:"
-    }
-    
-    public func appStore(action: AppStore.Action) -> Bool {
-        return appCaller.launch(AppStore(), action: action)
+        public let fallbackURL = "http:"
+        
+        public init() {}
     }
 }
 
diff --git a/Appz/Appz/Apps/Instagram.swift b/Appz/Appz/Apps/Instagram.swift
index dca39b1..4643c65 100644
--- a/Appz/Appz/Apps/Instagram.swift
+++ b/Appz/Appz/Apps/Instagram.swift
@@ -13,12 +13,12 @@ public extension Applications {
     
     public struct Instagram: ExternalApplication {
         
+        public typealias ActionType = Applications.Instagram.Action
+
         public let scheme = "instagram:"
-        public let fallbackURL: String? = "https://instagram.com/"
-    }
-    
-    public func instagram(action: Instagram.Action) -> Bool {
-        return appCaller.launch(Instagram(), action: action)
+        public let fallbackURL = "https://instagram.com/"
+
+        public init() {}
     }
 }
 
diff --git a/Appz/Appz/Apps/Mail.swift b/Appz/Appz/Apps/Mail.swift
index 5152fab..bfbaec8 100644
--- a/Appz/Appz/Apps/Mail.swift
+++ b/Appz/Appz/Apps/Mail.swift
@@ -13,12 +13,12 @@ public extension Applications {
     
     public struct Mail: ExternalApplication {
         
+        public typealias ActionType = Applications.Mail.Action
+
         public let scheme = "mailto:"
-        public let fallbackURL: String? = ""
-    }
-    
-    public func mail(action: Mail.Action) -> Bool {
-        return appCaller.launch(Mail(), action: action)
+        public let fallbackURL = ""
+
+        public init() {}
     }
 }
 
diff --git a/Appz/Appz/Apps/Messages.swift b/Appz/Appz/Apps/Messages.swift
index 2efca2b..dd4ef3e 100644
--- a/Appz/Appz/Apps/Messages.swift
+++ b/Appz/Appz/Apps/Messages.swift
@@ -13,12 +13,12 @@ public extension Applications {
     
     public struct Messages: ExternalApplication {
         
+        public typealias ActionType = Applications.Messages.Action
+
         public let scheme = "sms:"
-        public let fallbackURL: String? = ""
-    }
-    
-    public func messages(action: Messages.Action) -> Bool {
-        return appCaller.launch(Messages(), action: action)
+        public let fallbackURL = ""
+
+        public init() {}
     }
 }
 
diff --git a/Appz/Appz/Apps/Settings.swift b/Appz/Appz/Apps/Settings.swift
index 3254723..fce1487 100644
--- a/Appz/Appz/Apps/Settings.swift
+++ b/Appz/Appz/Apps/Settings.swift
@@ -13,12 +13,12 @@ public extension Applications {
     
     public struct AppSettings: ExternalApplication {
         
+        public typealias ActionType = Applications.AppSettings.Action
+
         public let scheme = UIApplicationOpenSettingsURLString
-        public let fallbackURL: String? = nil
-    }
-    
-    func appSettings(action: AppSettings.Action) -> Bool {
-        return appCaller.launch(AppSettings(), action: action)
+        public let fallbackURL = ""
+        
+        public init() {}
     }
 }
 
diff --git a/Appz/Appz/Apps/Twitter.swift b/Appz/Appz/Apps/Twitter.swift
index 72bec23..b1a98b3 100644
--- a/Appz/Appz/Apps/Twitter.swift
+++ b/Appz/Appz/Apps/Twitter.swift
@@ -13,12 +13,12 @@ public extension Applications {
     
     public struct Twitter: ExternalApplication {
         
+        public typealias ActionType = Applications.Twitter.Action
+
         public let scheme = "twitter:"
-        public let fallbackURL: String? = "https://twitter.com/"
-    }
-    
-    public func twitter(action: Twitter.Action) -> Bool {
-        return appCaller.launch(Twitter(), action: action)
+        public let fallbackURL = "https://twitter.com/"
+
+        public init() {}
     }
 }
 
diff --git a/Appz/Appz/Entity/ApplicationCaller.swift b/Appz/Appz/Entity/ApplicationCaller.swift
index c78eec6..a32cccb 100644
--- a/Appz/Appz/Entity/ApplicationCaller.swift
+++ b/Appz/Appz/Entity/ApplicationCaller.swift
@@ -20,11 +20,16 @@ public protocol ApplicationCaller {
 
 public extension ApplicationCaller {
     
-    public var open: Applications {
-        return Applications(appCaller: self)
+    public func canOpen<E: ExternalApplication>(externalApp: E) -> Bool {
+
+        if let baseURL = NSURL(string: externalApp.scheme) {
+            return canOpenURL(baseURL)
+        }
+        
+        return false
     }
     
-    func launch(externalApp: ExternalApplication, action: ExternalApplicationAction) -> Bool {
+    public func open<E: ExternalApplication>(externalApp: E, action: E.ActionType) -> Bool {
         
         let scheme = externalApp.scheme + "//"
         let baseURL = NSURL(string: scheme)
@@ -36,9 +41,7 @@ public extension ApplicationCaller {
             return openURL(url)
         }
         
-        if let fallbackURL = externalApp.fallbackURL,
-            let url = paths.web.appendToURL(fallbackURL)
-        {
+        if let url = paths.web.appendToURL(externalApp.fallbackURL) {
             return openURL(url)
         }
         
diff --git a/Appz/Appz/Entity/ExternalApplication.swift b/Appz/Appz/Entity/ExternalApplication.swift
index e2ba24d..b9c9ec6 100644
--- a/Appz/Appz/Entity/ExternalApplication.swift
+++ b/Appz/Appz/Entity/ExternalApplication.swift
@@ -14,8 +14,10 @@ import Foundation
  */
 public protocol ExternalApplication {
     
+    typealias ActionType: ExternalApplicationAction
+    
     var scheme: String { get }
-    var fallbackURL: String? { get }
+    var fallbackURL: String { get }
 }
 
 public struct ActionPaths {
diff --git a/Appz/AppzTests/AppsTests/SettingsTests.swift b/Appz/AppzTests/AppsTests/SettingsTests.swift
index a7d31c5..db7c8be 100644
--- a/Appz/AppzTests/AppsTests/SettingsTests.swift
+++ b/Appz/AppzTests/AppsTests/SettingsTests.swift
@@ -18,7 +18,7 @@ class SettingsTests: XCTestCase {
         
         let appSettings = Applications.AppSettings()
         XCTAssertEqual(appSettings.scheme, "app-settings:")
-        XCTAssertEqual(appSettings.fallbackURL, nil)
+        XCTAssertEqual(appSettings.fallbackURL, "")
     }
     
     func testOpenHome() {
diff --git a/Appz/AppzTests/EntityTests/ApplicationCallerTests.swift b/Appz/AppzTests/EntityTests/ApplicationCallerTests.swift
index b0550fa..d4b8547 100644
--- a/Appz/AppzTests/EntityTests/ApplicationCallerTests.swift
+++ b/Appz/AppzTests/EntityTests/ApplicationCallerTests.swift
@@ -14,8 +14,10 @@ class ApplicationCallerTests: XCTestCase {
     
     private struct SampleApp: ExternalApplication {
         
+        typealias ActionType = SampleAction
+        
         let scheme = "test:"
-        let fallbackURL: String? = "http://google.com/"
+        let fallbackURL = "http://google.com/"
     }
     
     private struct SampleAction: ExternalApplicationAction {
@@ -45,7 +47,7 @@ class ApplicationCallerTests: XCTestCase {
     func testQuery() {
         
         let sampleApp = SampleApp()
-        appCallerMock.launch(sampleApp, action: SampleAction())
+        appCallerMock.open(sampleApp, action: SampleAction())
         
         let expectedURL = NSURL(string: "\(sampleApp.scheme)//")
         XCTAssertEqual(appCallerMock.queriedURLs[0], expectedURL)
@@ -58,7 +60,7 @@ class ApplicationCallerTests: XCTestCase {
         sampleAction.paths.app = Path()
         
         appCallerMock.canOpenURLs = true
-        appCallerMock.launch(sampleApp, action: sampleAction)
+        appCallerMock.open(sampleApp, action: sampleAction)
         
         let expectedURL = NSURL(string: "\(sampleApp.scheme)//")
         XCTAssertEqual(appCallerMock.openedURLs[0], expectedURL)
@@ -70,7 +72,7 @@ class ApplicationCallerTests: XCTestCase {
         let sampleAction = SampleAction()
 
         appCallerMock.canOpenURLs = true
-        appCallerMock.launch(sampleApp, action: sampleAction)
+        appCallerMock.open(sampleApp, action: sampleAction)
         
         let expectedURL = sampleAction.paths.app.appendToURL("\(sampleApp.scheme)//")
         XCTAssertEqual(appCallerMock.openedURLs[0], expectedURL)
@@ -82,9 +84,9 @@ class ApplicationCallerTests: XCTestCase {
         let sampleAction = SampleAction()
         
         appCallerMock.canOpenURLs = false
-        appCallerMock.launch(sampleApp, action: sampleAction)
+        appCallerMock.open(sampleApp, action: sampleAction)
         
-        let expectedURL = sampleAction.paths.web.appendToURL(sampleApp.fallbackURL!)
+        let expectedURL = sampleAction.paths.web.appendToURL(sampleApp.fallbackURL)
         XCTAssertEqual(appCallerMock.openedURLs[0], expectedURL)
     }
 }
diff --git a/Playground/Playground.playground/Contents.swift b/Playground/Playground.playground/Contents.swift
index 3bbe8f0..2c8f749 100644
--- a/Playground/Playground.playground/Contents.swift
+++ b/Playground/Playground.playground/Contents.swift
@@ -10,15 +10,15 @@ import Appz
 //: Concise syntax to trigger deeplinking
 
 let app = UIApplication.sharedApplication()
-app.open.appStore(.Account(id: "395107918"))
-app.open.twitter(.UserHandle("mazyod"))
-app.open.appSettings(.Open)
+app.canOpen(Applications.Instagram())
+app.open(Applications.AppStore(), action: .Account(id: "395107918"))
+app.open(Applications.AppSettings(), action: .Open)
 
 //: Transparent web fallback
 
 // In case the user doesn't have twitter installed, it will fallback to
 // https://twitter.com/statuses/2
-app.open.twitter(.Status(id: "2"))
+app.open(Applications.Twitter(), action: .Status(id: "2"))
 
 //: Add your own application
 
@@ -29,33 +29,29 @@ extension Applications {
     // conforms to "ExternalApplication"
     struct MyApp: ExternalApplication {
         
+        typealias ActionType = Applications.MyApp.Action
+        
         let scheme = "myapp:"
-        let fallbackURL: String? = nil
-    }
-    // Add a function to easily access your app from 
-    // the application caller
-    func myApp(action: MyApp.Action) -> Bool {
-        return appCaller.launch(MyApp(), action: action)
+        let fallbackURL = ""
     }
 }
 // Finally, you define the actions your app supports
 extension Applications.MyApp {
     
-    enum Action {
+    enum Action: ExternalApplicationAction {
+        
         case Open
-    }
-}
-
-extension Applications.MyApp.Action: ExternalApplicationAction {
-    // Each action should provide an app path and web path to be
-    // added to the associated URL
-    var paths: ActionPaths {
         
-        switch self {
-        case .Open:
-            return ActionPaths()
+        // Each action should provide an app path and web path to be
+        // added to the associated URL
+        var paths: ActionPaths {
+            
+            switch self {
+            case .Open:
+                return ActionPaths()
+            }
         }
     }
 }
 
-app.open.myApp(.Open)
+app.open(Applications.MyApp(), action: .Open)
diff --git a/Playground/Playground.playground/contents.xcplayground b/Playground/Playground.playground/contents.xcplayground
index 712c41f..3596865 100644
--- a/Playground/Playground.playground/contents.xcplayground
+++ b/Playground/Playground.playground/contents.xcplayground
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<playground version='5.0' target-platform='ios' display-mode='raw' executeOnSourceChanges='false'>
+<playground version='5.0' target-platform='ios' display-mode='raw'>
     <timeline fileName='timeline.xctimeline'/>
 </playground>
\ No newline at end of file
diff --git a/README.md b/README.md
index 9378e6f..90bbc2b 100644
--- a/README.md
+++ b/README.md
@@ -40,9 +40,9 @@ __Concise syntax to trigger deep linking:__
 
 ```swift
 let app = UIApplication.sharedApplication()
-app.open.appStore(.Account(id: "395107918"))
-app.open.twitter(.UserHandle("mazyod"))
-app.open.appSettings(.Open)
+app.canOpen(Applications.Instagram())
+app.open(Applications.AppStore(), action: .Account(id: "395107918"))
+app.open(Applications.AppSettings(), action: .Open)
 ```
 
 __Transparent web fallback:__
@@ -50,49 +50,45 @@ __Transparent web fallback:__
 ```swift
 // In case the user doesn't have twitter installed, it will fallback to
 // https://twitter.com/statuses/2
-app.open.twitter(.Status(id: "2"))
+app.open(Applications.Twitter(), action: .Status(id: "2"))
 ```
 
 __Add your applications:__
 
 ```swift
 // Applications are recommended to be part of the 
-// "Applicaitons" namespace
+// "Applications" namespace
 extension Applications {
     // Define your application as a type that
     // conforms to "ExternalApplication"
     struct MyApp: ExternalApplication {
         
+        typealias ActionType = Applications.MyApp.Action
+
         let scheme = "myapp:"
-        let fallbackURL: String? = nil
-    }
-    // Add a function to easily access your app from 
-    // the application caller
-    func myApp(action: MyApp.Action) -> Bool {
-        return appCaller.launch(MyApp(), action: action)
+        let fallbackURL = ""
     }
 }
-// Finally, you define the actions your app supports
+// Then, you define the actions your app supports
 extension Applications.MyApp {
     
-    enum Action {
+    enum Action: ExternalApplicationAction {
+      
         case Open
-    }
-}
 
-extension Applications.MyApp.Action: ExternalApplicationAction {
-    // Each action should provide an app path and web path to be
-    // added to the associated URL
-    var paths: ActionPaths {
-        
-        switch self {
-        case .Open:
-            return ActionPaths()
+        // Each action should provide an app path and web path to be
+        // added to the associated URL
+        var paths: ActionPaths {
+            
+            switch self {
+            case .Open:
+                return ActionPaths()
+            }
         }
     }
 }
 
-app.open.myApp(.Open)
+app.open(Applications.MyApp(), action: .Open)
 ```
 
 __Supported Apps (for now!):__
@@ -100,10 +96,10 @@ __Supported Apps (for now!):__
 App | Actions
 ----|--------
 AppSettings | Open
-Mail | Compose
-Messages | SMS
 AppStore | Developer, App
 Instagram | Open, Camera, Media, Username, Location, Tag
+Mail | Compose
+Messages | SMS
 Twitter | Status, UserHandle, UserId, List, Post, Search, Timeline, Mentions, Messages
 
 ## Getting Started
