Swift URLからパラメーターを取り出す

ディープリンク対応する際にこちらリポジトリを参考に実装しました。

urlのパラメーター(具体的にはuser?id=0 user/:id的なやつ)はpathParameterとqueryParameterと呼ばれているらしいです。

使用例

URLの文字列をpattern文字列を指定してパースします。 パース結果のオブジェクトはqueryParamsとpathParamsプロパティーを持ちます。 これらはSwift4.2で追加されたDynamicMemberLookUpを使ったPrameterオブジェクトとして定義しています。

例えば通常のパラメーターの場合はqueryParamsで取得できます

let pattern: "myapp://user"
let actual: "myapp://user?id=123"

ParsedObject(url: actual, pattern: pattern).queryParams.id // "123"

パターン文字列:idのように指定すると pathParamsの値にマップされてpathParams.idとして取得できます。

let pattern: "myapp://user/:id"
let actual: "myapp://user/123"

ParsedObject(url: actual, pattern: pattern).pathParams.id // "123"

先述した通り内部でDynamicMemberLookUpを利用することで、 パターンの文字列がそのまま変数名になります。

let pattern: "myapp://user/:userId"
let actual: "myapp://user/123"

ParsedObject(url: actual, pattern: pattern).pathParams.userId // "123"

どちらもある場合はこんな感じです。

let pattern: "myapp://user/:id/"
let actual: "myapp://user/xxx?id=yyy"


ParsedObject(url: actual, pattern: pattern).queryParams.id // "yyyy"
ParsedObject(url: actual, pattern: pattern).pathParams.id // "xxxx"

ソース

Ap

struct ParsedURL {
    let openUrlOption: OpenURLOption
    let pathParams: Parameter
    let queryParams: Parameter

    init?(_ patternUrl: URL, url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) {
        guard url.scheme == patternUrl.scheme,
                    url.host == patternUrl.host ,
                    patternUrl.pathComponents.count == url.pathComponents.count else {
            return nil
        }
        let keywordPrefix = ":"
        // init pathParams
        do {
            var params: [String: String] = [:]
            // compare host
            if patternHost.hasPrefix(keywordPrefix) {
                let key = String(patternHost[keywordPrefix.endIndex...])
                params[key] = url.host
            }
            // compare path components
            for (component, pathCompoent) in zip(patternUrl.pathComponents, url.pathComponents) {
                if component.hasPrefix(keywordPrefix) {
                    let key = String(component[keywordPrefix.endIndex...])
                    params[key] = pathCompoent
                } else if component != pathCompoent {
                    return nil
                }
            }
            self.pathParams = Parameter(values: params)
        }
        // init queryParams
        do {
            if let components = URLComponents(url: url, resolvingAgainstBaseURL: true) {
                var params: [String: String] = [:]
                components.queryItems?.forEach { params[$0.name] = $0.value }
                self.queryParams = Parameter(values: params)
            } else {
                self.queryParams = Parameter(values: [:])
            }
        }
        self.openUrlOption = OpenURLOption(options: options)
    }
}

extension Scheme {
    @dynamicMemberLookup
    struct Parameter {
        let values: [String: String]

        subscript(dynamicMember name: String) -> String? {
            return values[name]
        }
    }

    struct OpenURLOption {
        let sourceApplication: String?
        let annotation: UIDocumentInteractionController?
        let openInPlace: Bool

        init(options: [UIApplication.OpenURLOptionsKey: Any]) {
            self.sourceApplication = options[.sourceApplication] as? String
            self.annotation = options[.annotation] as? UIDocumentInteractionController
            self.openInPlace = options[.openInPlace] as? Bool ?? false
        }
    }
}