URL パスの末尾スラッシュを保持する拡張を追加

hotwired/hotwire-native-ios

iOS 15 では URL.path が末尾スラッシュを除去する一方、iOS 16 以降は保持します。この差異を吸収し、パスマッチングの一貫性を保つための拡張が導入されました。

背景

iOS と Android でパス設定の扱いに差があり、iOS は末尾スラッシュを自動的に無視できたのに対し、Android では明示的に両パターンを記述する必要がありました。Issue #114 では、/add/add/ のように同一エンドポイントをカバーするために Android 側でパターンを二重に書くことが求められ、設定が煩雑になると指摘されています。iOS でもバージョン間で URL.path の挙動が変わるため、同一コードベースで動作が統一できず、開発者が環境差に惑わされるリスクがありました。

技術的な変更

URL 拡張に pathPreservingSlash プロパティを追加し、OS バージョンに応じたパス取得ロジックを統一しました。このプロパティは iOS 16 以降では path() を呼び出し、それ以前では URLComponents を用いて末尾スラッシュを保持した文字列を返します。

import Foundation

extension URL {
    /// A computed property that returns the URL's path, ensuring a trailing slash is preserved if it exists.
    ///
    /// This property addresses the behavioral difference between `URL.path` in iOS 15 (which strips trailing slashes)
    /// and `URL.path()` in iOS 16+ (which preserves them), providing a single, consistent behavior across all OS versions.
    var pathPreservingSlash: String {
        if #available(iOS 16.0, *) {
            return path()
        } else {
            return URLComponents(string: absoluteString)?.path ?? ""
        }
    }
}

PathConfiguration のパス取得ロジックが url.path から url.pathPreservingSlash へ置き換えられました。クエリ文字列を含む場合でも同様に新プロパティが使用され、末尾スラッシュが失われることなくマッチングが行われます。

diff:Source/Turbo/Path Configuration/PathConfiguration.swift
@@ -60,9 +60,9 @@ public final class PathConfiguration {
- if Hotwire.config.pathConfiguration.matchQueryStrings, let query = url.query {
- return properties(for: "\(url.path)?\(query)")
- }
- return properties(for: url.path)
+ if Hotwire.config.pathConfiguration.matchQueryStrings, let query = url.query {
+ return properties(for: "\(url.pathPreservingSlash)?\(query)")
+ }
+ return properties(for: url.pathPreservingSlash)

新規テスト URL_UtilsTests が追加され、末尾スラッシュの有無と iOS バージョン双方で期待通りの文字列が返ることを検証しています。iOS 16 以上とそれ以前のシナリオをそれぞれアサートし、互換性レイヤーの正確性を保証します。

@testable import HotwireNative
import XCTest

class URL_UtilsTests: XCTestCase {
    func testCompatiblePath_withoutTrailingSlash() {
        let url = URL(string: "https://www.example.com/path/to/resource")!
        let expectedPath = "/path/to/resource"
        XCTAssertEqual(url.pathPreservingSlash, expectedPath)
    }

    func testCompatiblePath_withTrailingSlash() {
        let url = URL(string: "https://www.example.com/path/to/directory/")!
        let expectedPath = "/path/to/directory/"
        if #available(iOS 16.0, *) {
            XCTAssertEqual(url.pathPreservingSlash, expectedPath)
        } else {
            XCTAssertEqual(url.path, "/path/to/directory", "iOS 15 drops the trailing slash.")
            XCTAssertEqual(url.pathPreservingSlash, expectedPath)
        }
    }
}

設計判断

既存の URL.path を直接変更せず、互換性レイヤーとして拡張を提供する方針が採られましたpathPreservingSlash は利用側が明示的に呼び出すだけで済み、従来コードへの破壊的変更を避けられます。iOS のバージョンチェックを内部に組み込むことで、将来的に URL.path() が標準化された場合でも同一実装で対応可能です。

PathConfiguration のみが新プロパティを利用することで、他のモジュールへの影響は最小限に抑えられ、iOS 15 以前のデバイスでも同一設定で動作します。この局所的な変更はテストで網羅され、リグレッションリスクが低減されたことから、保守性と機能統一の観点で妥当な設計判断と言えます。

まとめ

pathPreservingSlash の導入により、iOS のバージョン間で異なる URL パスの扱いを統一し、Android との設定差異を解消しました。PathConfiguration が新プロパティを利用するだけで、既存コードの互換性を保ちつつ開発者体験が向上します。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
b171d787

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的変更・設計判断・まとめが揃っており、総論→各論→結論の構成が明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックのファイル名付きハイライトは正しい形式ですが、GitHubリンクがPRのみでIssue #114 がリンク化されていません。また、PRリンクが "[PR #211]" ではなく "[PR #211]" という形式で、完全に仕様どおりではありません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

iOS/Android のURL処理に関する専門的な記述で、エンジニア向けに適切です。初心者向けの余計な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論・結論の段落で構成され、トピックセンテンスで始まり、1段落1トピック・長さも適切です。段落間は空行で区切られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロックは Diff に記載された変更と完全に一致しています。

技術用語の正確性 ⚠ WARNING

技術用語の正確な使用

PR タイトルに "backslash" とありますが、記事では "slash"(スラッシュ)を正しく使用しています。用語の統一がややずれていますが、誤用ではありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的説明は Diff と PR 内容に基づき正確であり、誤った主張はありません。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の全ての主張は PR の情報(Issue #114 の背景、コード変更)で裏付けられています。根拠のない推測や数値的主張はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #211、Issue 番号 #114、ファイル名・パスはすべて正確です。

タイトル・説明との一致 ⚠ WARNING

記事タイトル・説明とPR内容の一致

記事タイトルは変更内容を示すが、PR タイトルの "feedback" という語が反映されていません。内容的には一致していますが、タイトルの表現が若干ずれています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

外部のバージョンサポート情報やリリース予定など、PR に記載されていない情報は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現のずれはなく、PR の記述と整合しています。