Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Crossroad - 最高のCustom URL Schemeルーティングを支える技術2021

giginet
December 15, 2021

Crossroad - 最高のCustom URL Schemeルーティングを支える技術2021

[iPhone Dev Sapporo feat. HAKATA.swift - connpass](https://hakata-swift.connpass.com/event/232775/)

giginet

December 15, 2021
Tweet

More Decks by giginet

Other Decks in Technology

Transcript

  1. ࣗݾ঺հ • @giginet (Twitter/GitHub) • ΫοΫύου iOSςοΫϦʔυ • ٕज़ސ໰ɿtaskey /

    MoneyForward • Core Contributor: fastlane / Carthage / XcodeGen etc... 2
  2. Before class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication,

    open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { let url = URL(string: "pokedex://pokemons/25")! let components = url.pathComponents if url.scheme == "pokedex" { if url.host == "pokemons" { if components.count == 2, let pokedexID: Int = Int(components[1]) { presentPokemonDetailViewController(of: pokedexID) return true } else if ( ... ) { } else { // ... } } } } } 11
  3. Before class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication,

    open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { let url = URL(string: "pokedex://pokemons/25")! let components = url.pathComponents if url.scheme == "pokedex" { if url.host == "pokemons" { if components.count == 2, let pokedexID: Int = Int(components[1]) { presentPokemonDetailViewController(of: pokedexID) return true } else if ( ... ) { } else { // ... } } } } } 11
  4. Before class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication,

    open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { let url = URL(string: "pokedex://pokemons/25")! let components = url.pathComponents if url.scheme == "pokedex" { if url.host == "pokemons" { if components.count == 2, let pokedexID: Int = Int(components[1]) { presentPokemonDetailViewController(of: pokedexID) return true } else if ( ... ) { } else { // ... } } } } } 11
  5. After import Crossroad let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) {

    registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { router.openIfPossible(url, options: options) } } 13
  6. After import Crossroad let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) {

    registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { router.openIfPossible(url, options: options) } } 13
  7. After import Crossroad let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) {

    registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { router.openIfPossible(url, options: options) } } 13
  8. After import Crossroad let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) {

    registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { router.openIfPossible(url, options: options) } } 13
  9. After import Crossroad let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) {

    registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { router.openIfPossible(url, options: options) } } 13
  10. After import Crossroad let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) {

    registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { router.openIfPossible(url, options: options) } } 13
  11. աڈͷ঺հهࣄ • iOSΞϓϦͷେن໛ͳCustom URL SchemeΛࢧ͑Δٕज़ - Ϋο Ϋύου։ൃऀϒϩά • https://techlife.cookpad.com/entry/2018/06/01/113000

    • Custom URL SchemeΛࢧ͑Δٕज़ - Speaker Deck • https://speakerdeck.com/giginet/custom-url-schemewozhi- eruji-shu?slide=10 16
  12. ॻ͖΍͍͢DSL Before let router = DefaultRouter(scheme: "pokedex") router.register([ ("pokedex://pokemons/:pokedexID", {

    context in guard let pokedexID: Int = try? context.arguments(for: "pokedexID") else { return false } presentPokemonDetailViewController(of: pokedexID) return true }) ]) • [(String, (Context) -> Bool)]Ͱهड़ • ੒ޭɺࣦഊΛBoolͰฦ͍ͯͨ͠ 19
  13. ॻ͖΍͍͢DSL Before let router = DefaultRouter(scheme: "pokedex") router.register([ ("pokedex://pokemons/:pokedexID", {

    context in guard let pokedexID: Int = try? context.arguments(for: "pokedexID") else { return false } presentPokemonDetailViewController(of: pokedexID) return true }) ]) • [(String, (Context) -> Bool)]Ͱهड़ • ੒ޭɺࣦഊΛBoolͰฦ͍ͯͨ͠ 19
  14. After let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) { registry in

    registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } • ResultBuilderΛ࢖ͬͯ௚ײతʹॻ͚ΔΑ͏ʹͳͬͨ 20
  15. Result Builder • Swift 5.4͔Βར༻Մೳʹͳͬͨ৽͍͠ݴޠػೳ • એݴతͳAPIઃܭΛ࣮ݱ͢Δ͜ͱ͕ग़དྷΔ • SwiftUIͷએݴతUIΛ࣮ݱ͢ΔͨΊʹݴޠػೳʹ૊Έࠐ·Εͨ VStack(alignment:

    .leading) { Text("Turtle Rock") .font(.title) HStack { Text("Joshua Tree National Park") .font(.subheadline) Spacer() Text("California") .font(.subheadline) } } 21
  16. Result BuilderͰDSLΛઃܭ͠Α͏ • ResultBuilderͷ࢖͍ํΛ࿩͠ग़͢ͱ1͙࣌ؒΒ͍͔͔Δ • Write a DSL in Swift

    using result builders - WWDC21 - Videos - Apple Developer • WWDCͷ͜ͷηογϣϯ͕Φεεϝ 22
  17. Router { Route("/pokemon/:pokedexID") { context in // do something }

    Route("/pokemons") { context in // do something } // ... } • ϒϩοΫͷதͷRouteͷཏྻΛ [Route] ͱͯ͠औΓ͍ͨ 23
  18. @resultBuilder struct RouteBuilder { static func buildBlock(_ components: Route...) ->

    [Route] { components } } struct Router { init(@RouteBuilder _ routeBuilder: () -> [Route]) { self.routes = routeBuilder() } } 24
  19. @resultBuilder struct RouteBuilder { static func buildBlock(_ components: Route...) ->

    [Route] { components } } struct Router { init(@RouteBuilder _ routeBuilder: () -> [Route]) { self.routes = routeBuilder() } } 24
  20. 2. Universal Link΍Deferred Deep Linkͱͷܦ࿏ڞ༗ • ݱ࣮ͷϏδωεʹ͸Custom URL SchemeͷଞʹɺUniversal Link΍Firebase

    Dynamic Links(Deferred Deep Link)ͳͲɺ༷ʑͳ ΞϓϦૹ٬ख๏͕͋Δ • ࠓ·ͰͷCrossroad͸ɺෳ਺ͷྲྀೖܦ࿏Λѻ͏͜ͱ͕Ͱ͖ͳ ͔ͬͨ 26
  21. Router͕ड͚෇͚ΔϦϯΫݩΛෳ਺औΕΔΑ͏ʹ let router = try! DefaultRouter(accepting: [ .customURLScheme("pokedex"), .universalLink(URL(string: "https://my-pokedex.com")!)

    ]) { registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } 28
  22. Router͕ड͚෇͚ΔϦϯΫݩΛෳ਺औΕΔΑ͏ʹ let router = try! DefaultRouter(accepting: [ .customURLScheme("pokedex"), .universalLink(URL(string: "https://my-pokedex.com")!)

    ]) { registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } } 28
  23. Group let router = try! DefaultRouter(accepting: [ .customURLScheme("pokedex"), .universalLink(URL(string: "https://my-pokedex.com")!)

    ]) { registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } // ͜ͷGroupʹғ·Εͨϧʔτఆٛ͸Custom URL Schemeܦ༝Ͱ͔͠ΞΫηεͰ͖ͳ͍ registry.group(accepting: [.customURLScheme("pokedex")]) { group in group.route("/moves/:moveID") { context in // ... } } } 29
  24. Group let router = try! DefaultRouter(accepting: [ .customURLScheme("pokedex"), .universalLink(URL(string: "https://my-pokedex.com")!)

    ]) { registry in registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } // ͜ͷGroupʹғ·Εͨϧʔτఆٛ͸Custom URL Schemeܦ༝Ͱ͔͠ΞΫηεͰ͖ͳ͍ registry.group(accepting: [.customURLScheme("pokedex")]) { group in group.route("/moves/:moveID") { context in // ... } } } 29
  25. ྫ1ɿෳ਺ͷϧʔτ͕ॏෳ͍ͯ͠Δέʔε let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) { registry in

    registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } // ... // ͏͔ͬΓಉ͡ఆ͕ٛॏෳ registry.route("/pokemons/:pokedexID") { context in } } 32
  26. ྫ1ɿෳ਺ͷϧʔτ͕ॏෳ͍ͯ͠Δέʔε let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) { registry in

    registry.route("/pokemons/:pokedexID") { context in let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) } // ... // ͏͔ͬΓಉ͡ఆ͕ٛॏෳ registry.route("/pokemons/:pokedexID") { context in } } 32
  27. ྫ2ɿͦ΋ͦ΋ड͚෇͚ͳ͍ϦϯΫݩΛGroupʹ͍ͯ͠ Δέʔε let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) { registry

    in registry.group(accepting: [.customURLScheme("unknown")]) { group in group.route("/moves/:moveID") { context in // ... } } } 33
  28. ྫ2ɿͦ΋ͦ΋ड͚෇͚ͳ͍ϦϯΫݩΛGroupʹ͍ͯ͠ Δέʔε let router = try! DefaultRouter(accepting: [.customURLScheme("pokedex")]) { registry

    in registry.group(accepting: [.customURLScheme("unknown")]) { group in group.route("/moves/:moveID") { context in // ... } } } 33
  29. 1. Swift ConcurrencyʹΑΔϝΠϯεϨουอূ • جຊతʹURLεΩʔϜͷల։͸ɺଞͷϏϡʔʹભҠ͢ΔͳͲɺ UIͷૢ࡞Λߦ͏ͨΊɺϝΠϯεϨουͰಈ͘΂͖ registry.route("/pokemons/:pokedexID") { context in

    let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) // ϝΠϯεϨουͰಈ͍ͯཉ͍͠ } 35
  30. • HandlerͱopenIfPossibleͷܕΛ @MainActor ʹ͢Δ typealias Handler = @MainActor (Context<UserInfo>) throws

    -> Void @MainActor public func openIfPossible(_ url: URL, userInfo: UserInfo) -> Bool 37
  31. • ͜ΕʹΑͬͯopenIfPossible͸ϝΠϯεϨου͔Β͔࣮͠ߦͰ ͖ͳ͘ͳΔͨΊɺAppDelegateࣗମ΋@MainActorͱ͢Δ @MainActor class AppDelegate: UIResponder, UIApplicationDelegate { func

    application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { return router.openIfPossible(url, options: options) } } • Routerʹ౉͞Εͨhandler͕ϝΠϯεϨουͰಈ͘͜ͱ͕อূ͞ ΕΔΑ͏ʹͳΓ҆શ ! • https://github.com/giginet/Crossroad/pull/43 38
  32. 2. DocCରԠ • Documentation Compiler • ؆୯ʹΦϯϥΠϯυΩϡϝϯτ͕࡞ΕΔͷͰ༻ҙ͍ͨ͠ • ӳޠͱ͍͏೉͍͠ݴޠ͕ॻ͚ͳ͍ͷͰ͕͔͔͍࣌ؒͬͯΔ •

    Θ͍Θ͍DocC ~ waiwai-docc ~ - Speaker Deck • https://speakerdeck.com/giginet/waiwaidocc-waiwai-docc 39
  33. 3. ෳ਺ϧʔςΟϯάରԠ registry.route(["/pokemons/:pokedexID", "/:pokedexID"]) { context in let pokedexID: Int

    = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) // ϝΠϯεϨουͰಈ͍ͯཉ͍͠ } • pokedex://pokemons/25Ͱ΋pokedex://25Ͱ΋ड͚෇͚ΔΑ͏ʹ • ๭ϨγϐΞϓϦͷϧʔςΟϯά͕ෳࡶ͗ͯ͢࢖͍͍ͨ 40