こんにちは。技術部モバイル基盤グループの@giginetです。
今回は、iOSアプリでCustom URL Schemeを簡単に処理するライブラリを公開しましたので紹介します。
Custom URL Schemeは、アプリの特定の画面に遷移させることができるリンク(ディープリンク)を提供する機能です。
アプリ開発をしていると、Custom URL Schemeを用いたディープリンクを実装したい需要は多いでしょう。 特にクックパッドのような、ブラウザ版を提供するWebサービスですと、アプリとWebページの行き来のため非常に多くのCustom URL Schemeを処理する必要が出てきます。
現に、クックパッドアプリでは、30以上のパターンが遷移先として実装されています。
渡ってきたURL
のパーサーを愚直に書いていくのは、コードの記述量も増えますし、どのようなURL Schemeが有効なのか簡単に見通すことは難しいです。
Crossroad
そこで、複雑なCustom URL Schemeのルーティングを簡単に実現するライブラリをOSSとして公開しました。
例えば、あなたがiOS上で「ポケモンずかん」を実装する仕事を請け負ったとしましょう。
Crossroadを用いると、以下のような記述でCustom URL Schemeのルーティングが行えます。
letrouter= DefaultRouter(scheme:"pokedex") router.register([ ("pokedex://pokemons", { context inlettype:Type? = context.parameter(for:"type") presentPokedexListViewController(for:type) returntrue }), ("pokedex://pokemons/:pokedexID", { context inguardletpokedexID:Int= try? context.arguments(for:"pokedexID") else { returnfalse } guardletpokemon= Pokedex.find(by:pokedexID) else { returnfalse } presentPokemonDetailViewController(of:pokemon) returntrue }), ]) router.openIfPossible(url)
このように、Ruby on Railsのroutes.rb
のようなルーティングを記述することができます。
この仕組みをクックパッドアプリでは、1年以上前から運用していたのですが、今回、別のアプリでも使いやすい形で提供するためにOSS化しました。
同様のライブラリはいくつか公開されていますが、Crossroadはこれらに比べ、パラメータをType-Safeに、そして簡単に取り扱うことができます。
使い方
Crossroadの基本的な使い方を見ていきましょう。
URLのルーティング
iOSでは、Custom URL Schemeからアプリが起動されると、UIApplicationDelegate
の application(_:open:options:)
が呼び出されます。
基本的な使い方は、AppDelegate
で、ルーティングを定義した Router
を生成し、そこでopenIfPossible
を呼び出すだけです。
import Crossroad classAppDelegate:UIApplicationDelegate { letrouter:DefaultRouter= { letrouter= DefaultRouter(scheme:"scheme") router.register([ ("scheme://search", { _ inreturntrue }), // ... ]) return router }() funcapplication(_ app:UIApplication, open url:URL, options:[UIApplicationOpenURLOptionsKey: Any]) ->Bool { return router.openIfPossible(url, options:options) } }
:
から始まるパスは任意の文字列にマッチし、あとでマッチした値を参照できます。
URLパターンは定義順に上からマッチするかどうかが判定され、ブロックから true
を返された時点で探索を終了します。
複数のURLパターンにマッチしうる場合も、最初にtrue
を返した物のみが実行されます。
パラメータの取得
:
から始まるパスにマッチした文字列は Argument
として扱われ、ブロックから取得することができます。
("pokedex://pokemons/:pokedexID", { context in// URLからポケモンずかん番号を取得guardletpokedexID:Int= try? context.arguments(for:"pokedexID") else { returnfalse } // 該当するポケモンを取得するguardletpokemon= Pokedex.find(by:pokedexID) else { returnfalse } // ポケモン詳細画面を表示する presentPokemonDetailViewController(of:pokemon) returntrue })
Argument
はGenericsを利用しているので、任意の型として受け取ることができます。
例えば、pokedex://pokemons/25
のURL Schemeからアプリを起動した場合、ずかん番号25番のポケモンが表示されます。
enumの値を取得する
Argument
を利用することで、それぞれのポケモンの詳細画面へ遷移するURL Schemeを実装することができました。
今度はポケモンを検索する画面を作ってみましょう。
URLのクエリとして渡された値は Parameter
として扱われ、Argument
と同様にContext
から取得することができます。
ここで、ポケモンのタイプを示すenum Type
を定義してみましょう。
Crossroadでは、Extractable
というプロトコルに準拠させることで、任意の型をContext
から返却することができます。
enumType:String, Extractable { case normal // ノーマルタイプcase fire // ほのおタイプcase water // みずタイプcase grass // くさタイプ// ... }
enumを表す型であるRawRepresentable
は、すでにExtractable
に準拠しているため、これだけで文字列をenumにマッピングすることができます。
("pokedex://pokemons", { context inlettype:Type? = context.parameters(for:"type") // ポケモン一覧画面を表示する presentPokemonListViewController(of:type) returntrue })
これで、pokedex://pokemons?type=fire
というURL Schemeからアプリを起動すると、ほのおポケモンのみを表示する画面へ遷移することができます。
一般的な検索画面を実装する場合は、キーワードや並び順などをパラメータで受け取る実装が考えられるでしょう。
pokedex://search?keyword=ピカチュウ&order=asc
複数の値を取得する
ポケモンずかんを実装するに当たって、今度は複合タイプのポケモンをURL Schemeから検索したいという需要が出てくるでしょう。
Crossroadは、パラメータに渡されたカンマ区切りの文字列を配列としてマッピングする機能も提供しています。
// pokedex://pokemons?types=water,grasslettypes:[Type]? = context.parameters(for:"types") // [.water, .grass]
これは、Swift 4.1から利用可能になった、Conditional Conformanceを用いて、[Extractable]
をExtractable
に準拠させることで実現しています。
extensionArray:Extractablewhere Array.Element:Extractable { staticfuncextract(from string:String) ->[Element]? { letcomponents= string.split(separator:",") return components .map { String($0) } .compactMap(Element.extract(from:)) } }
独自の型を取得する
もちろん、独自の型を取得することもできます。Context
から取得したい型をExtractable
に準拠させましょう。
structPokemon:Extractable { letname:Stringstaticfuncextract(from string:String) ->Pokemon? { return Pokemon(name:string) } } // pokedex://pokemons/:nameletpokemon:Pokemon= try? context.arguments(for:"name")
このように、Crossroadでは、柔軟にパスやクエリパラメータの取得を行うことができます。
Dynamic Member Lookupを使ったインターフェイス
最後に、Swift 4.2から実装される新たな言語機能であるDynamic Member Lookupを使ったインターフェイスの構想を紹介します。
Dynamic Member Lookupは、動的なプロパティ生成を提供するシンタックスシュガーです。
クラスや構造体に@dynamicMemberLookup
を宣言することで、ランタイムで評価されるプロパティを生成することができます。
Dynamic Member Lookupを宣言すると、subscript(dynamicMember:)
の実装が要求され、プロパティアクセスを行ったときに、プロパティ名が引数に渡され実行されます。
@dynamicMemberLookupstructContainer { letvalues:[String: Any]subscript<T>(dynamicMember member:String) ->T? { ifletvalue= values[member] { return value as? T } } } letcontainer= Container(values:["name": "Pikachu"]) letname:String= container.name // Pikachu
本稿執筆時点では、Swift 4.2の正式版はまだリリースされていませんが、Swift.orgからdevelopmentのToolchainをダウンロードすることで、Xcode 9.3でも利用することができました *1。
この機能をCrossroad.Context
に適用してみると、以下のように Argument
を取得できるようになりました。
// match pokedex://pokemons/:pokedexIDletpokedexID:Int? = context.arguments.pokedexID
この実装はまだmasterへマージしていませんが、別ブランチで公開しているので、興味のある方は見てみてください。
まとめ
今回はCustom URL Schemeを簡単にルーティングするライブラリを紹介しました。 ぜひ利用を検討してみてください。もちろんPull Requestもお待ちしております。
技術部モバイル基盤グループでは、OSSを通して問題解決をしていきたいエンジニアを募集しています。
*1:普通にビルドすることはできますが、静的解析の時点ではシンタックスエラーが発生します