STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

iOS アプリからローカルサーバーへ接続する道のり

STORES ブランドアプリの iOS アプリ側を開発している Megabits です。

STORES ブランドアプリはアプリとサーバーの連動で成り立っています。なので、アプリを開発するとき、サーバーへ接続してデバッグすることがほどんどです。逆に、バックエンドを開発する際、アプリを操作して動作確認することも多いです。Staging のサーバーや Production のサーバーに接続してテストすることができます。アプリにはこのような DebugView が存在していて、 Staging と Production を切り替えられます。

環境切り替え画面

しかし、現状だとローカルでバックエンドのコードを編集する時、アプリからローカル環境に接続してテストすることができません。もし連動してテストしたい場合は、Staging にデプロイしないといけません。その場合、仕事をしているエンジニア全員に影響を与えるので、ローカルで自分だけ見える変更をテストすることができません。なので、ローカルサーバーに繋いでテストする機能を追加したいです。

バックエンドサーバーをローカルで実行する時、スクリプトで host ファイルを編集してますので、特定の URL をアクセスすると、管理画面を開くことができます。つまり、アプリからローカルサーバーへ接続するには、全ての API の URL ホストを修正すればできます。

しかし、その前に解決すべき問題が一つあります。ローカルのサーバーなので、https の SSL 証明書が無効なので、アプリから直接接続することができません。その無効な証明書を無視する必要があります。

info.plist の編集

iOS では、特定サイトの https 安全問題を無視することができます。以前の iOS バージョンでは、一括で無視できますが、今だと特定の host を設定する必要があります。今回追加した内容は、以下になります。

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
    <dict>
        <key>example.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

info.plist

これでアプリ全体にかかってる制限をなくすことができました。特に、サブドメインを許可することを忘れないようにしましょう。

しかし、ライブラリー個別にまだ制限がありますので、それも調整しましょう。

Alamofire

Alamofire では、ServerTrustManager というオブジェクトが存在しています。URL 別で、制限を調整することができます。説明はここです:

alamofire.github.io

ServerTrustManager(evaluators: [
    urlStaging: DefaultTrustEvaluator(),
    urlLocal: DisabledTrustEvaluator()
])

ここで、urlStaging をそのまま検証し、urlLocal を検証なしにします。

Session を作る際、それを引用します。

let session = Alamofire.Session(configuration: configuration, serverTrustManager: manager)

これで、ローカルサーバーへの API 通信ができました。

KingFisher

KingFisher では、AuthenticationChallengeResponsible というオブジェクトが存在しています。説明はここです:

swiftpackageindex.com

final class LocalEnvChallengeResponder: NSObject, AuthenticationChallengeResponsible, Sendable {
    nonisolated static let shared = LocalEnvChallengeResponder()
    func downloader(
        _ downloader: ImageDownloader,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        if let trust = challenge.protectionSpace.serverTrust, Appmaker.manager.debugRepository.serverEnvironment == .local {
            return (URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: trust))
        } else {
            return (URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
        }
    }
}

画像のダウンロード先は複雑なので、ここで直接デバッグ環境で判断しました。デバッグ環境であれば、証明書を無視します。もらった serverTrust をそのまま信用することで、制限をなくしました。

return (URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: trust))

URL からローカルかどうかを判断したい場合、challenge.protectionSpace.host から判断することもできます。

WKWebView

最後、アプリには、幾つかの WKWebView が使われています、その部分も制限解除する必要があります。

KingFisher のところと似たようなコードを WKNavigationDelegate に追加することで解決できます。ここに説明があります。

developer.apple.com

func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping @MainActor (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    if let trust = challenge.protectionSpace.serverTrust, Appmaker.manager.debugRepository.serverEnvironment == .local {
        completionHandler(.useCredential, URLCredential(trust: trust))
    } else {
        completionHandler(.performDefaultHandling, nil)
    }
}

最後、全ての API をローカル host に設定するロジックを完了すると、ローカルサーバーに接続できるようになります。

昔の iOS と比べると、現在の iOS はかなり安全になっていると感じています。あえて不安全な動作をさせたい時の手数もかかります。漏れなくネット通信をするライブラリの設定を調整するのが大事です。そして、プロダクション環境に影響しないように、ローカルサーバーに繋ぐ時だけ証明書を無視していることも確認しましょう。とういうことで、お読みいただきありがとうございます。また次回のブログで会いましょう。