URL パスの末尾スラッシュを保持する拡張を追加
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 が新プロパティを利用するだけで、既存コードの互換性を保ちつつ開発者体験が向上します。