事の経緯
あるとき、URLの初期化が絡んだUTのケースでテストが失敗するようになった。
しかし、該当する実装はいじっていない。
謎挙動だ。。
でも影響してないからヨシ!としてしばらく目を背けていました。
これにちゃんと向き合って深ぼってみたよという話です。
問題
テストコードを簡単に作ってみました。
import UIKit
func test() {
let path = "https://www.google.com/"
guard let url = URL(string: path) else {
print("nil")
return
}
print(url) // https://www.google.com/
}
test()
これは正常系なので期待値は想定通り。
次に、本来URLに渡したら値を返さないはずの値を入れてみました。
//let path = "https://www.google.com/"
let path1 = "^-^" // URLで許容されていない記号
let path2 = "ほげ" // 日本語
let path3 = "" // 空欄
path1の結果: "%5E-%5E"
path2の結果: "%E3%81%BB%E3%81%92"
path3の結果: "nil"
本来ならnilが表示されて欲しいはずが、URLエンコードされて表示されてしまっていました。
かろうじて空文字は期待通りになりましたが、落ちていたUTのケース的に不正なURLで問題ないかを確認するようなケースだったので、じゃあ空文字にすればテスト通るから良いよね!ということにはなりませんでした。
解決策
ここで一度公式のドキュメントを見直してみると、URLの初期化に関するページで重要な記述を見つけました。
For apps linked on or after iOS 17 and aligned OS versions, URL parsing has updated from the obsolete RFC 1738/1808 parsing to the same RFC 3986 parsing as URLComponents. This unifies the parsing behaviors of the URL and URLComponents APIs. Now, URL automatically percent- and IDNA-encodes invalid characters to help create a valid URL.
要するに、
- iOS17以降のバージョンだと従来のURL解析は廃止されてURLComponentsと同じ解析方法を用いるように更新された
- 無効な文字を自動的にエンコードするようになった
といったことが言及されています。
じゃあiOS17ではどうすれば従来通りの挙動になるのか?というところですが、init(string:encodingInvalidCharacters:)
を使うと良いです。
encodingInvalidCharacters
をfalse
にすることで、URL文字列が厳密に有効であるかを確認するようになります。
下記のように修正すると、無事に従来通りの挙動になりました。
func test() {
let path = "^-^"
guard let url = URL(string: path, encodingInvalidCharacters: false) else {
print("nil") // 早期リターンのケースに入り、nilが表示される
return
}
print(url)
}
とはいえ、この初期化のメソッドはiOS17以降から利用できるものになるので、iOS16以前のバージョンも対応している場合は条件を分岐して利用するのが良いでしょう。
if #available(iOS 17.0, *) {
let url = URL(string: path, encodingInvalidCharacters: false)
// 何らかの処理
} else {
let url = URL(string: path)
// 何らかの処理
}
まとめ
挙動に困惑しましたが、蓋を開けてみれば原因と解決策はシンプルなものでした。
同じように困った人がいれば助けになると幸いです。
(こんな記事書いといてもうすぐiOS18が出そう...次のOSの準備をせねば...!)