Slide 1

Slide 1 text

Custom URL Scheme Λࢧ͑Δٕज़ #potatotips 53 @giginet

Slide 2

Slide 2 text

୭ • @giginet • ϞόΠϧج൫ @ Cookpad Inc. • iOSDCొஃ͠·͢ • ৄղFastfile •

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

30Ҏ্ͷURL Scheme

Slide 6

Slide 6 text

໰୊఺ • ར༻ՄೳͳSchemeͷҰཡੑ͕௿͍ • URLதʹؚ·ΕΔจࣈྻ΍Query Parameterͷ ύʔε͕໘౗

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

pokedex://pokemons/25

Slide 9

Slide 9 text

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) } else if ( ... ) { } else { // ... } } }

Slide 10

Slide 10 text

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) } else if ( ... ) { } else { // ... } } }

Slide 11

Slide 11 text

giginet/Crossroad • URLSchemeΛϧʔςΟϯάͯ͠URL͔Β؆୯ ʹ஋Λऔಘ͢Δ΍ͭ

Slide 12

Slide 12 text

import Crossroad class AppDelegate: UIApplicationDelegate { let router: DefaultRouter = { let router = DefaultRouter(scheme: "scheme") router.register([ ("scheme://search", { _ in return true }), // ... ]) return router }() func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey: Any]) -> Bool { return router.openIfPossible(url, options: options) } }

Slide 13

Slide 13 text

let router = DefaultRouter(scheme: "pokedex") router.register([ ("pokedex://pokemons", { context in let type: Type? = context.parameter(for: "type") presentPokedexListViewController(for: type) return true }), ("pokedex://pokemons/:pokedexID", { context in guard let pokedexID: Int = try? context.arguments(for: "pokedexID") else { return false } guard let pokemon = Pokedex.find(by: pokedexID) else { return false } presentPokemonDetailViewController(of: pokedexID) return true }), ]) router.openIfPossible(url)

Slide 14

Slide 14 text

let router = DefaultRouter(scheme: "pokedex") router.register([ ("pokedex://pokemons", { context in let type: Type? = context.parameter(for: "type") presentPokedexListViewController(for: type) return true }), ("pokedex://pokemons/:pokedexID", { context in guard let pokedexID: Int = try? context.arguments(for: "pokedexID") else { return false } guard let pokemon = Pokedex.find(by: pokedexID) else { return false } presentPokemonDetailViewController(of: pokedexID) return true }), ]) router.openIfPossible(url) pokedex://pokemons/25 25

Slide 15

Slide 15 text

public protocol Extractable { static func extract(from: String) -> Self? } extension Int: Extractable { public static func extract(from string: String) -> Int? { return Int(string) } } let pokedexID: Int? = try? context.arguments(for: "pokedexID")

Slide 16

Slide 16 text

pokedex://pokemons?type=fire

Slide 17

Slide 17 text

enum Type: String, Extractable { case normal // ϊʔϚϧλΠϓ case fire // ΄ͷ͓λΠϓ case water // ΈͣλΠϓ case grass // ͘͞λΠϓ // ... }

Slide 18

Slide 18 text

public extension RawRepresentable where Self: Extractable, Self.RawValue == String { public static func extract(from string: String) -> Self? { return Self(rawValue: string) } }

Slide 19

Slide 19 text

// match pokedex://pokemons?type=fire ("pokedex://pokemons", { context in let type: Type? = context.parameters(for: "type") // ϙέϞϯҰཡը໘Λදࣔ͢Δ presentPokemonListViewController(of: type) return true })

Slide 20

Slide 20 text

pokedex://pokemons?type=water,grass

Slide 21

Slide 21 text

// pokedex://pokemons?types=water,grass let types: [Type]? = context.parameters(for: "types") // [.water, .grass]

Slide 22

Slide 22 text

extension Array: Extractable where Array.Element: Extractable { static func extract(from string: String) -> [Element]? { let components = string.split(separator: ",") return components .map { String($0) } .compactMap(Element.extract(from:)) } }

Slide 23

Slide 23 text

Dynamic Member Lookup

Slide 24

Slide 24 text

@DynamicMemberLookup • ಈతͳϓϩύςΟੜ੒͕ՄೳʹͳΔ౶ҥߏจ • Swift 4.2͔Βར༻Մೳ

Slide 25

Slide 25 text

@dynamicMemberLookup struct Container { let values: [String: Any] subscript(dynamicMember member: String) -> T? { if let value = values[member] { return value as? T } } } let container = Container(values: ["name": "Pikachu"]) let name: String = container.name // Pikachu

Slide 26

Slide 26 text

// match pokedex://pokemons/:pokedexID // before let pokedexID: Int? = try? context.arguments(for: "pokedexID") // after let pokedexID: Int? = context.arguments.pokedexID

Slide 27

Slide 27 text

@dynamicMemberLookup public struct ArgumentContainer { private let arguments: [String: String] public func fetch(for key: String) throws -> T { if let argument = arguments[key] { if let value = T.extract(from: argument) { return value } } throw Error.parsingArgumentFailed } public subscript(dynamicMember member: String) -> T? { return try? fetch(for: member) } }

Slide 28

Slide 28 text

https://techlife.cookpad.com/entry/ 2018/06/01/113000

Slide 29

Slide 29 text

࠷ۙ

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

·ͱΊ • େن໛ͳURL Schemeʹ͸Crossroadศར • Swift 4.2ͷ@DynamicMemberLookup • OSS΍ͬͯΔͱϩΰ࡞ͬͯ͘ΕΔਓ͕͍ͯ༏͠ ͍ੈք • ⭐͍ͩ͘͞

Slide 32

Slide 32 text

͝ਗ਼ௌ ͋Γ͕ͱ͏͍͟͝·ͨ͠