Quantcast
Channel: クックパッド開発者ブログ
Viewing all articles
Browse latest Browse all 726

iOSアプリの大規模なCustom URL Schemeを支える技術

$
0
0

こんにちは。技術部モバイル基盤グループの@です。

今回は、iOSアプリでCustom URL Schemeを簡単に処理するライブラリを公開しましたので紹介します。

Custom URL Schemeは、アプリの特定の画面に遷移させることができるリンク(ディープリンク)を提供する機能です。

f:id:gigi-net:20180530205333g:plain

アプリ開発をしていると、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からアプリが起動されると、UIApplicationDelegateapplication(_: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を通して問題解決をしていきたいエンジニアを募集しています。

iOS アプリケーションエンジニア(開発基盤)

Android アプリケーションエンジニア(開発基盤)

*1:普通にビルドすることはできますが、静的解析の時点ではシンタックスエラーが発生します


Viewing all articles
Browse latest Browse all 726

Trending Articles