Upgrade to Pro — share decks privately, control downloads, hide ads and more …

肥大化しがちなアプリの起動経路を整理する

d_date
August 31, 2018

 肥大化しがちなアプリの起動経路を整理する

2018/08/31 iOSDC Japan 2018

d_date

August 31, 2018
Tweet

More Decks by d_date

Other Decks in Programming

Transcript

  1. ϓογϡ௨஌ • Local Push Notification • Remote Push Notification ~

    iOS 9: AppDelegate iOS 10 ~: UserNotifiaction.framework
  2. Universal Links (Deep Link) • ΞϓϦΠϯετʔϧࡁΈͷ৔߹ • Web → App

    • User Activityѻ͍(URL SchemeʹΑΔભҠͱಉ͡ʣ • activityType͸NSUserActivityTypeBrowsingWeb
  3. Universal Links (Deep Link) • ΞϓϦະΠϯετʔϧͷ৔߹ • Web → App

    Store → App • didFinishLaunching͕ݺ͹ΕΔʢ௨ৗىಈʣ • URLΛ։͍ͯ೚ҙͷϖʔδ΁ʢopenURL)
  4. Spotlight • ݕࡧ͔Βͷىಈ • ݕࡧ݁Ռ (CSSearchableItemActionType) • ΞϓϦ಺ݕࡧʢiOS 10 ~ʣ

    (CSQueryContinuationActionType) • ͲͪΒ΋User Activityѻ͍ʹͳΔ
  5. ΞϓϦͷىಈܦ࿏ • ௨ৗىಈ • ϓογϡ௨஌ʢLocal / Remoteʣ • σΟʔϓϦϯΫ •

    Core Spotlight (Result Action / Query ) • Home Screen Quick Action • Widget
  6. NDA

  7. ΞϓϦͷىಈܦ࿏ • ௨ৗىಈ • ϓογϡ௨஌ʢLocal / Remoteʣ • σΟʔϓϦϯΫ •

    Core Spotlight (Result Action / Query ) • Home Screen Quick Action • Siri Shortcut • etc…
  8. ΞϓϦͷىಈܦ࿏ App Delegate ௨ৗىಈ ϓογϡ௨஌ʢ ~ iOS 9 ) σΟʔϓϦϯΫ

    Core Spotlight Home Screen Quick Action Siri Shortcut (Intents) User Notification ϓογϡ௨஌ʢiOS 10 ~) etc…
  9. App Delegate import UIKit import UserNotifications import CoreSpotlight @UIApplicationMain class

    AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var tabBarController: UITabBarController? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { self.window = UIWindow(frame: UIScreen.main.bounds) self.tabBarController = UITabBarController() tabBarController.viewControllers = [FirstViewController(), SecondViewController(), ThirdViewController()] window?.rootViewController = tabBarController return true } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { switch userActivity.activityType { case NSUserActivityTypeBrowsingWeb: guard let url = userActivity.webpageURL else { fatalError("unreachable") } let detailViewController = DetailViewController() detailViewController.identifier = url.lastPathComponent (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) case CSSearchableItemActionType: guard let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { fatalError("unreachable")} let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) case CSQueryContinuationActionType: guard let query = userActivity.userInfo?[CSSearchQueryString] as? String else { fatalError("unreachable") } let detailViewController = DetailViewController() detailViewController.identifier = query (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) case "com.d-date.CoordinatorExample.intent": // Siri shortcut let identifier = userActivity.userInfo?["signiture"] as? String let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) default: break } return true } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { if url.scheme == "coordinator-example-widget" { let identifier = url.lastPathComponent let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) } else if url.scheme == "adjustSchemeExample" { let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) } else if url.scheme == "FirebaseDynamicLinksExmaple" { let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) } } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { let text = shortcutItem.type.replacingOccurrences(of: bundleIdentifier + ".", with: "").lowercased() switch text { case "a": let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) case "b": let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) case "c": let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) default: break } } } extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let detailViewController = DetailViewController() detailViewController.identifier = identifier (tabBarController?.viewControllers?[0] as? UINavigationController)?.pushViewController(detailViewController, animated: true) completionHandler() } } • ※ιʔείʔυ͸ΠϝʔδͰ͢
  10. ىಈܦ࿏ΛEnumͰఆٛ import UIKit import CoreSpotlight import UserNotifications enum LaunchType {

    case normal case notification(_ notification: UNNotificationRequest) case userActivity(_ userActivity: NSUserActivity) case openURL(_ url: URL) case shortcutItem(_ shortcutItem: UIApplicationShortcutItem) } ܕͰྲྀೖݩΛ͢΂ͯ໢ཏ
  11. τϥοΩϯάΠϕϯτΛEnumͰఆٛ struct LaunchTracker { enum Event: Equatable { case normal

    case localNotification(identifier: String) case remoteNotification(identifier: String) case deepLink(url: URL) case intent(word: String) case spotlight(resultIdentifier: String) case spotlight(query: String) case widget(identifier: String) case homeScreen(type: String) }
  12. τϥοΩϯάΠϕϯτΛEnumͰఆٛ struct LaunchTracker { enum Event: Equatable { case normal

    case localNotification(identifier: String) case remoteNotification(identifier: String) case deepLink(url: URL) case intent(word: String) case spotlight(resultIdentifier: String) case spotlight(query: String) case widget(identifier: String) case homeScreen(type: String) } ςετ͢Δ͜ͱ͕લఏͷ࣮૷
  13. IN OUT Normal Notification UserActivity shortcutItem URL normal localNotification remoteNotification)

    deepLink intent spotlight(result: String) spotlight(query: String) widget homeScreen
  14. launchType -> Event @discardableResult static func track(launchType: LaunchType) -> Event?

    { switch launchType { case .normal: return send(event: .normal) case .notification(let request): if request.trigger is UNPushNotificationTrigger { let event: Event = .remoteNotification(identifier: request.identifier) return send(event: event) } else if request.trigger is UNTimeIntervalNotificationTrigger { let event: Event = send(event: .localNotification(identifier: request.identifier)) return event }
  15. launchType -> Event case .userActivity(let activity): switch activity.activityType { case

    NSUserActivityTypeBrowsingWeb: guard let url = activity.webpageURL else { fatalError("unreachable") } return send(event: .deepLink(url: url)) case CSSearchableItemActionType: guard let identifier = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { fatalError("unreachable") } return send(event: .spotlight(resultIdentifier: identifier)) case CSQueryContinuationActionType: guard let query = activity.userInfo?[CSSearchQueryString] as? String else { fatalError("unreachable") } return send(event: .spotlight(query: query)) case "com.d-date.CoordinatorExample.intent": // Siri shortcut return send(event: .intent(word: "word")) }
  16. launchType -> Event case .openURL(let url): if url.scheme == "coordinator-example-widget"

    { let identifier = url.lastPathComponent return send(event: .widget(identifier: identifier)) } else if url.scheme == "adjustSchemeExample" { //TODO: replace your adjust url scheme return send(event: .deepLink(url: url)) } else if url.scheme == "FirebaseDynamicLinksExmaple" { //TODO: handle your FDL return send(event: .deepLink(url: url)) } return nil // untracked any other urls case .shortcutItem(let item): return send(event: .homeScreen(type: item.type)) } return nil }
  17. IN OUT Normal Notification UserActivity shortcutItem URL normal localNotification remoteNotification)

    deepLink intent spotlight(result: String) spotlight(query: String) widget homeScreen
  18. IN OUT Normal Notification UserActivity shortcutItem URL normal localNotification remoteNotification)

    deepLink intent spotlight(result: String) spotlight(query: String) widget homeScreen ͜͜Λςετ͢Δ
  19. ςετͯ͠ΈΑ͏ func testLaunchLocalPushNotification() { let identifier = "testLocalNotification" let request

    = UNNotificationRequest( identifier: identifier, content: .init(), trigger: UNTimeIntervalNotificationTrigger.init(timeInterval: 30, repeats: false) ) guard let event = LaunchTracker.track(launchType: .notification(request)) else { XCTFail() return } XCTAssertEqual(event, LaunchTracker.Event.localNotification(identifier: identifier)) }
  20. ςετͯ͠ΈΑ͏ func testLaunchLocalPushNotification() { let identifier = "testLocalNotification" let request

    = UNNotificationRequest( identifier: identifier, content: .init(), trigger: UNTimeIntervalNotificationTrigger.init(timeInterval: 30, repeats: false) ) guard let event = LaunchTracker.track(launchType: .notification(request)) else { XCTFail() return } XCTAssertEqual(event, LaunchTracker.Event.localNotification(identifier: identifier)) } μϛʔΛ࡞Δ
  21. ςετͯ͠ΈΑ͏ func testLaunchLocalPushNotification() { let identifier = "testLocalNotification" let request

    = UNNotificationRequest( identifier: identifier, content: .init(), trigger: UNTimeIntervalNotificationTrigger.init(timeInterval: 30, repeats: false) ) guard let event = LaunchTracker.track(launchType: .notification(request)) else { XCTFail() return } XCTAssertEqual(event, LaunchTracker.Event.localNotification(identifier: identifier)) } Notification͔Βىಈͨ͠ͱ͖ͷEventΛऔಘ
  22. ςετͯ͠ΈΑ͏ func testLaunchLocalPushNotification() { let identifier = "testLocalNotification" let request

    = UNNotificationRequest( identifier: identifier, content: .init(), trigger: UNTimeIntervalNotificationTrigger.init(timeInterval: 30, repeats: false) ) guard let event = LaunchTracker.track(launchType: .notification(request)) else { XCTFail() return } XCTAssertEqual(event, LaunchTracker.Event.localNotification(identifier: identifier)) } औಘͨ͠Πϕϯτͱɺਖ਼ղΛൺֱ
  23. ςετͯ͠ΈΑ͏ func testLaunchDeepLink() { let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) let

    url = URL(string: "https://example.coordinator.com/123456")! userActivity.webpageURL = url guard let event = LaunchTracker.track(launchType: .userActivity(userActivity)) else { XCTFail() return } XCTAssertEqual(event, .deepLink(url: url)) }
  24. ςετͯ͠ΈΑ͏ func testLaunchSpotlightClickResult() { let userActivity = NSUserActivity(activityType: CSSearchableItemActionType) userActivity.userInfo

    = [CSSearchableItemActivityIdentifier: "123456"] guard let event = LaunchTracker.track(launchType: .userActivity(userActivity)) else { XCTFail() return } XCTAssertEqual(event, .spotlight(resultIdentifier: "123456")) }
  25. ςετͯ͠ΈΑ͏ func testLaunchRemotePushNotification() { let identifier = "testLocalNotification" let request

    = UNNotificationRequest( identifier: identifier, content: .init(), trigger: UNPushNotificationTrigger.init() ) guard let event = LaunchTracker.track(launchType: .notification(request)) else { XCTFail() return } XCTAssertEqual(event, LaunchTracker.Event.localNotification(identifier: identifier)) }
  26. ςετͯ͠ΈΑ͏ func testLaunchRemotePushNotification() { let identifier = "testLocalNotification" let request

    = UNNotificationRequest( identifier: identifier, content: .init(), trigger: UNPushNotificationTrigger.init() ) guard let event = LaunchTracker.track(launchType: .notification(request)) else { XCTFail() return } XCTAssertEqual(event, LaunchTracker.Event.localNotification(identifier: identifier)) }
  27. Initializer͕ެ։͞Ε͍ͯͳ͍ @available(iOS 10.0, *) open class UNNotificationTrigger : NSObject, NSCopying,

    NSSecureCoding { open var repeats: Bool { get } } @available(iOS 10.0, *) open class UNPushNotificationTrigger : UNNotificationTrigger { }
  28. import UIKit import CoreSpotlight import UserNotifications @UIApplicationMain class AppDelegate: UIResponder,

    UIApplicationDelegate { var window: UIWindow? private var appCoordinator: AppCoordinator? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { self.window = UIWindow(frame: UIScreen.main.bounds) self.appCoordinator = AppCoordinator(window: window!, launchType: .normal) appCoordinator!.start() return true } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { self.window = UIWindow(frame: UIScreen.main.bounds) let type: LaunchType = .userActivity(userActivity) self.appCoordinator = AppCoordinator(window: window!, launchType: type) appCoordinator!.start() return true } func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { self.window = UIWindow(frame: UIScreen.main.bounds) let type: LaunchType = .openURL(url) self.appCoordinator = AppCoordinator(window: window!, launchType: type) appCoordinator!.start() return true } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { self.window = UIWindow(frame: UIScreen.main.bounds) let type: LaunchType = .shortcutItem(shortcutItem) self.appCoordinator = AppCoordinator(window: window!, launchType: type) appCoordinator!.start() completionHandler(true) } } extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { self.window = UIWindow(frame: UIScreen.main.bounds) let request = response.notification.request let type: LaunchType = .notification(request) self.appCoordinator = AppCoordinator(window: window!, launchType: type) appCoordinator!.start() completionHandler() } } ※ιʔείʔυ͸ΠϝʔδͰ͢