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

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

011714704c4a925e542d426d4cdaa4e3?s=47 giginet
December 15, 2021

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

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

011714704c4a925e542d426d4cdaa4e3?s=128

giginet

December 15, 2021
Tweet

More Decks by giginet

Other Decks in Technology

Transcript

  1. ࠷ߴͷCustom URL Scheme ϧʔςΟϯάΛࢧ͑Δٕज़2021 @giginet 2021/12/15 iPhone Dev Sapporo feat.

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

    MoneyForward • Core Contributor: fastlane / Carthage / XcodeGen etc... 2
  3. iPhone Dev Sapporo • ֶੜ࣌୅ɺࡳຈʹ͍ͨͷͰੲࢀՃ͍ͯͨ͠ • ॳࢀՃ͸2010೥ࠒͷݹࢀ ! • iPhone

    4 + iOS 4ͷࠒ • Swift? • ͭ·Γζϒζϒ΍ͬͯΔ " 3
  4. HAKATA.swift • աڈʹԿ౓͔തଟʹߦͬͯࢀՃ • ࠷ۙ͸MoneyForward෱ԬࢧࣾͷHAKATA.swiftӡӦͷΈͳ͞· ʹ͓ੈ࿩ʹͳ͍ͬͯΔ • ͭ·Γζϒζϒ΍ͬͯΔ ! 4

  5. ࠓ೔࿩͢͜ͱ • Crossroadͱ͍͏Custom URL SchemeͷϧʔςΟϯάΛྑ͍ײ ͡ʹ͢ΔϥΠϒϥϦΛ࠷ۙେܕΞοϓσʔτͨ͠ͷͰ঺հ͠· ͢ • - https://github.com/giginet/Crossroad

    5
  6. Custom URL Schemeͱ͸ • ΧελϜͨ͠URL͔ΒΞϓϦΛىಈͤ͞Δ͘͠Έ Defining a Custom URL Scheme

    for Your App | Apple Developer Documentation 6
  7. cookpad://search/ण࢘ 7

  8. Custom URL SchemeΛऔΓר͘໰୊ • ϧʔςΟϯά͕૿͑ΔͱϋϯυϦϯά͕ΊͪΌͪ͘Όେม • ࢓༷Λύοͱ೺Ѳͮ͠Β͍ • ͳΜ͔͍Ζ͍Ζ༻ޠ΍ભҠํ๏͕͋ͬͯ೉͍͠ •

    Deep Link?, Universal Link?, Custom URL Scheme?, Dynamic Links? 8
  9. ྫɿϙέϞϯ͔ͣΜ 9

  10. pokedex://pokemons/25 10

  11. 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
  12. 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
  13. 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
  14. ! 12

  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. • URL͔Βͷม਺ͷύʔε • ෳ਺URLεΩʔϜͷϧʔςΟϯά ͳͲ͕؆୯ʹߦ͑Δ ! 14

  22. ࠾༻ࣄྫ • ݟౕ͚͚ͭͨͩͳͷͰଞʹ͋ͬͨΒڭ͍͑ͯͩ͘͞ 15

  23. աڈͷ঺հهࣄ • 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
  24. ެ։͔Β3೥൒ɺ࠷ۙόʔδϣϯ4.0Λग़ͨ͠ ! 17

  25. ৽ػೳ 1. ॻ͖΍͍͢DSL 2. Universal Link΍Deferred Deep Linkͱͷܦ࿏ڞ༗ 3. ఆٛϛεΛ๷͙ͨΊͷ࢓૊Έ

    18
  26. ॻ͖΍͍͢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
  27. ॻ͖΍͍͢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
  28. 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
  29. 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
  30. Result BuilderͰDSLΛઃܭ͠Α͏ • ResultBuilderͷ࢖͍ํΛ࿩͠ग़͢ͱ1͙࣌ؒΒ͍͔͔Δ • Write a DSL in Swift

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

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

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

    [Route] { components } } struct Router { init(@RouteBuilder _ routeBuilder: () -> [Route]) { self.routes = routeBuilder() } } 24
  34. • CrossroadͷDSL͸΋ͬͱෳࡶ • ܕύϥϝʔλʔͷӅṭ • Group(ޙड़)ͷଘࡏɹ • ࣮૷ྫΛ͝ཡ͍ͩ͘͞ https://github.com/giginet/Crossroad/ blob/master/Sources/Crossroad/DSL.swift

    • ResultBuilderΛ࢖ͬͨDSLઃܭͷ஌ݟ͕ཷ·͔ͬͨΒ·ͨͲ͜ ͔Ͱ࿩͍ͨ͠ 25
  35. 2. Universal Link΍Deferred Deep Linkͱͷܦ࿏ڞ༗ • ݱ࣮ͷϏδωεʹ͸Custom URL SchemeͷଞʹɺUniversal Link΍Firebase

    Dynamic Links(Deferred Deep Link)ͳͲɺ༷ʑͳ ΞϓϦૹ٬ख๏͕͋Δ • ࠓ·ͰͷCrossroad͸ɺෳ਺ͷྲྀೖܦ࿏Λѻ͏͜ͱ͕Ͱ͖ͳ ͔ͬͨ 26
  36. ෳ਺ͷྲྀೖܦ࿏ͷϧʔςΟϯάΛଋͶͯɺಉ͡ڍಈΛ͍ͤͨ͞ • Custom URL Scheme • pokedex://pokemons/25 • Universal Link

    • https://my-pokedex.com/pokemons/25 27
  37. 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
  38. 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
  39. 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
  40. 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
  41. • ෳ਺ͷϦϯΫݩΛ1ͭͷϧʔλʔͰѻ͑ΔΑ͏ʹͳͬͨ • ϦϯΫݩຖʹΞΫηεͰ͖Δ͔Ͳ͏͔Λࡉ੍͔͘ޚͰ͖ΔΑ͏ ʹͳͬͨ(Group) 30

  42. 3. ఆٛϛεΛ๷͙ͨΊͷ࢓૊Έ • Routerͷੜ੒࣌ʹෆਖ਼ͳϧʔτఆٛΛݕ஌ͯ͠ྫ֎Λൃੜ͢Δ Α͏ʹͨ͠ • Routerͷinitilizer͕throws • ؒҧͬͨRouter͸ͦ΋ͦ΋ੜ੒Ͱ͖ͳ͘ͳͬͨͨΊɺఆٛϛε ʹΑΔϥϯλΠϜͰͷෆ۩߹͕๷͛Δ

    31
  43. ྫ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
  44. ྫ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
  45. ྫ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
  46. ྫ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
  47. Future works 34

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

    let pokedexID: Int = try context.argument(named: "pokedexID") presentPokedexDetailViewController(of: pokedexID) // ϝΠϯεϨουͰಈ͍ͯཉ͍͠ } 35
  49. • @MainActor Λ࢖͏͜ͱͰΩϨΠʹ࣮ݱͰ͖Δ • Xcode 13.2(ࡢ೔ϦϦʔεʂʂʂ)͔ΒSwift Concurrencyͷ backport͕དྷͨͷͰiOS 13.0+Ҏ্Ͱಈ࡞Մೳʹ 36

  50. • HandlerͱopenIfPossibleͷܕΛ @MainActor ʹ͢Δ typealias Handler = @MainActor (Context<UserInfo>) throws

    -> Void @MainActor public func openIfPossible(_ url: URL, userInfo: UserInfo) -> Bool 37
  51. • ͜ΕʹΑͬͯ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
  52. 2. DocCରԠ • Documentation Compiler • ؆୯ʹΦϯϥΠϯυΩϡϝϯτ͕࡞ΕΔͷͰ༻ҙ͍ͨ͠ • ӳޠͱ͍͏೉͍͠ݴޠ͕ॻ͚ͳ͍ͷͰ͕͔͔͍࣌ؒͬͯΔ •

    Θ͍Θ͍DocC ~ waiwai-docc ~ - Speaker Deck • https://speakerdeck.com/giginet/waiwaidocc-waiwai-docc 39
  53. 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
  54. 4. υΩϡϝϯςʔγϣϯࣗಈੜ੒ • SwiftSyntaxͰRouterͷDSLΛղੳͯ͠υΩϡϝϯτΛ࡞Δख๏ ͳͲΛߟ͍͑ͯΔ • ๭ϨγϐΞϓϦͰ͸ϧʔςΟϯά͕ଟ͗ͯ͢େมͳ͜ͱʹͳͬ ͍ͯͯ༗༻ 41

  55. ⭐ • ελʔ͍ͩ͘͞ • https://github.com/giginet/Crossroad • ͥͻ͝ར༻͍ͩ͘͞ 42

  56. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ ! ࣭ٙԠ౴ 43