- 投稿日:2021-01-08T21:52:10+09:00
iOSアプリで位置情報を取得するときに配慮する点をまとめてみた①
今回は、iOSアプリで位置情報を取得するときに配慮しなくてはいけない点を自分なりにまとめてみようと思います。
はじめに
私は今、現在地から飲食店を検索するアプリを開発しています。
※App Storeにリリースしているので良かったらダウンロードしてみて下さい!
ふっぴーくん自分の現在地から飲食店を検索するアプリなので、もちろん、現在地を取得しなくてはいけません。だったら、現在地を取得して「はい、終わり!」というシンプルなものではありませんでした。
まずは、現在地を取得するために最低限必要な手順から書いていきたいと思います。
手順
iOSアプリで位置情報を取得するためには、
1.プライバシー情報取得の許可
2.CoreLocationフレームワークをインポート、CLLocationManagerクラスを初期化
3.位置情報サービスを使用するために、ユーザーに許可をリクエスト
4.CLLocationManagerDelegateプロトコルを採用し、デリゲートメソッドを実装おおまかには、こんな感じの手順で位置情報が取得できます。
では、順を追って説明していきます。1.プライバシー情報取得の許可
iOSアプリでは、プライバシーに関わる情報を取得する際に許可が
必須
です。
位置情報もプライバシーに関わる情報なので、位置情報サービスを使用する前に許可を貰わないと使うことができません。許可を得るには、info.plistにて使いますよ!という意思表示を示しましょう。
Key:Location When In Use Usage Description
Value:任意の文字列(例:このアプリは位置情報サービスを使います。)
2.フレームワークをインポート、クラスを初期化
まずは、位置情報関連のサービスがアプリ内で使えるようにCoreLocationフレームワークをインポートします。
ViewControllerimport CoreLocationそして、CoreLocationフレームワークに含まれているCLLocationManagerクラスを初期化します。
このクラスは、位置情報の機能を管理
してくれています。このクラスを、メンバプロパティ(クラス内で定義されるプロパティ)として
ViewControllerで宣言しインスタンスを初期化していきます。ViewControllerimport CoreLocation class ViewController: UIViewController { //CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言 var locationManager: CLLocationManager = { // 取得精度の設定(1km以内の精度) locationManager.desiredAccuracy = kCLLocationAccuracyKilometer // 位置情報取得間隔(5m移動したら位置情報取得) locationManager.distanceFilter = 5 //インスタンスを初期化 var locationManager = CLLocationManager() return locationManager }() }CLLocationManagerクラスには、
位置情報取得の精度
や位置情報取得の間隔
など色々な設定を自分で調節できます。
宣言と同時に書いてあるdesiredAccuracy・distanceFilterプロパティの事ですね。自分のアプリに合った設定をすることで、バッテリーが長持ちするというメリットがあります。
位置情報サービスはかなりの電力を使いますので、こういう所も配慮しなくてはいけませんね。3.ユーザーに許可をリクエスト
位置情報サービスを使用するときは、
ユーザーに対して許可をリクエスト
しなくてはいけません。
今回は、アプリの使用中に許可をリクエストしたいのでCLLocationManagerクラスのメソッドrequestWhenInUseAuthorization()を使います。ViewControllerimport CoreLocation class ViewController: UIViewController { //CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言 var locationManager: CLLocationManager = { // 取得精度の設定(1km以内の精度) locationManager.desiredAccuracy = kCLLocationAccuracyKilometer // 位置情報取得間隔(5m移動したら位置情報取得) locationManager.distanceFilter = 5 //インスタンスを初期化 var locationManager = CLLocationManager() return locationManager }() override func viewDidLoad() { super.viewDidLoad() // ユーザーに許可をリクエスト locationManager.requestWhenInUseAuthorization() } }ここで一旦、ビルドしてみたいと思います。
そもそも端末の位置情報サービスが
オフ
になっているとこんな感じのアラートが表示されます。
設定
を選択することで設定アプリに画面遷移しますので、タップして位置情報サービスをオン
にしてみましょう。※ただし、アプリをインストール後の初回起動のみしか表示されません。
位置情報サービスを
オン
にしてから再度、アプリを起動すると、こんな感じのアラートが表示されます。
このアラートは、requestWhenInUseAuthorization()
を呼ぶと表示されます。
1度だけ許可
やAppの使用中は許可
などの選択肢がありますね。
これは位置情報サービスの認証ステータス
でいくつかの種類があります。
認証ステータスは、CLAuthorizationStatusにまとめており、
アプリを初めて起動した時はnotDetermined
になっています。なので現状、ビルドしたアプリは
notDetermined
状態という事ですね。分かりやすいように認証ステータスをまとめてみました↓
認証ステータス 意味 authorizedAlways 常に許可 authorizedWhenInUse Appの使用中は許可(1度だけ許可も含まれる) denied 許可しない restricted 端末の位置情報サービスが許可されていない notDetermined 位置情報サービスを使用できるかどうかを選択していない 因みに、requestWhenInUseAuthorization( )は
notDetermined以外だと呼ばれない
です。
これはリクエストを許可しているのにアラートが表示されてしまうのを防ぐためだと思われます。ユーザーにリクエストしたのはいいですが、まだ現時点では位置情報を取得することは出来ません。
4.デリゲートメソッドの実装
CLLocationManagerDelegateプロトコルを準拠し、デリゲートメソッドを実装していきます。
ViewControllerimport CoreLocation class ViewController: UIViewController { //CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言 var locationManager: CLLocationManager = { // 取得精度の設定(1km以内の精度) locationManager.desiredAccuracy = kCLLocationAccuracyKilometer // 位置情報取得間隔(5m移動したら位置情報取得) locationManager.distanceFilter = 5 //インスタンスを初期化 var locationManager = CLLocationManager() return locationManager }() override func viewDidLoad() { super.viewDidLoad() // ユーザーに許可をリクエスト locationManager.requestWhenInUseAuthorization() // デリゲート先を自分のViewControllerにする locationManager.delegate = self } } // ViewControllerにCLLocationManagerDelegateプロトコルを準拠 extention ViewController: CLLocationManagerDelegate { // CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるメソッド func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { // アプリの現在の認証ステータス let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: // 位置情報取得を開始 manager.startUpdatingLocation() case .notDetermined: // ユーザーに許可をリクエスト manager.requestWhenInUseAuthorization() case .denied: break case .restricted: break default: break } } // 位置情報を取得・更新したら呼ばれるメソッド func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { // 位置情報を取得する guard let gps = manager.location?.coordinate else { // 取得出来なかったらアラートを表示したりなど return } // 位置情報取得を停止 manager.stopUpdatingLocation() //経度と緯度を出力する let lat = gps.latitude let lng = gps.longitude print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))") } }
requestWhenInUseAuthorization()
によって、認証ステータスが決定したら
locationManagerDidChangeAuthorizationデリゲートメソッドが呼ばれ、そのメソッド内で処理していきます。とりあえずは、ユーザーが
.authorizedAlways
,.authorizedWhenInUse
を選択した場合の処理と
.notDetermined
を選択した場合の処理を記述しています。位置情報を取得するためには、
startUpdatingLocation()
メソッドを呼ばなければいけません。
このメソッドにより、位置情報取得を開始
してくれます。そして、位置情報を取得したらlocationManager(_: didUpdateLocations:)デリゲートメソッドが呼ばれ、
ここでやっと位置情報を取得することが出来ます。では、長くなりましたが、ここからが本題となります。
本題
題名にも書いてあるように、位置情報を取得する時には配慮しなくてはいけない点がいくつかあります。
まずは、端末の位置情報サービスの有無
です。端末の位置情報サービスがオフだった場合、「位置情報サービスをオンにして下さい」というアラートが
表示されると思いますが、アプリをインストール後の初回起動のみ
しか表示されません。
なので、2回目以降の起動時は端末の位置情報サービスがオフ
だった場合、何も起こりません。
もちろん、位置情報を取得することも出来ません。では、どうすれば良いのでしょうか?
端末の位置情報サービスの有無をチェックする
自分で端末の位置情報サービスの有無をチェックするコードを記述しなくてはいけません。
しかし、そんなに難しい訳ではなく、CLLocationManagerクラスには便利なメソッドがあります。
それは、locationServicesEnabled()です。このメソッドは、端末で位置情報サービスが有効になっているかどうかを示すBool値を返します。
trueの場合は位置情報サービスオン
、falseの場合は位置情報サービスオフ
では、このメソッドを先ほどのソースコードに組み込んでいきましょう。
画面表示後に呼ばれるViewDidApperメソッド
に記述します。そして、実際にアラートを表示したいので
私が過去にQiitaに投稿した記事のやり方でアラートを表示したいと思います↓
UIAlertControllerをファイルを分けて実装してみるViewControllerimport CoreLocation class ViewController: UIViewController { //CLLocationManagerクラスのインスタンスlocationManagerをメンバプロパティとして宣言 var locationManager: CLLocationManager = { // 取得精度の設定(1km以内の精度) locationManager.desiredAccuracy = kCLLocationAccuracyKilometer // 位置情報取得間隔(5m移動したら位置情報取得) locationManager.distanceFilter = 5 //インスタンスを初期化 var locationManager = CLLocationManager() return locationManager }() override func viewDidLoad() { super.viewDidLoad() // ユーザーに許可をリクエスト locationManager.requestWhenInUseAuthorization() // デリゲート先を自分のViewControllerクラスにする locationManager.delegate = self } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // 位置情報サービスがオフの場合 if !CLLocationManager.locationServicesEnabled() { // アラートを表示して位置情報サービスをオンにするようにユーザーに促したりする Alert.okAlert(title: "位置情報サービスを\nオンにして下さい", message: "「設定」アプリ ⇒「プライバシー」⇒「位置情報サービス」からオンにできます") } } } // ViewControllerにCLLocationManagerDelegateプロトコルを準拠 extention ViewController: CLLocationManagerDelegate { // CLLocationManagerクラスのインスタンス初期化および、認証ステータスが変更されたら呼ばれるメソッド func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { // アプリの現在の認証ステータス let status = manager.authorizationStatus switch status { case .authorizedAlways, .authorizedWhenInUse: // 位置情報取得を開始 manager.startUpdatingLocation() case .notDetermined: // ユーザーに許可をリクエスト manager.requestWhenInUseAuthorization() case .denied: break case .restricted: break default: break } } // 位置情報を取得・更新したら呼ばれるメソッド func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { // 位置情報を取得する guard let gps = manager.location?.coordinate else { // 取得出来なかったらアラートを表示したりなど return } // 位置情報取得を停止 manager.stopUpdatingLocation() //経度と緯度を出力する let lat = gps.latitude let lng = gps.longitude print("経度:\(String(describing: lat)), 緯度:\(String(describing: lng))") } }位置情報サービスを
オフ
の状態でビルドしてみると、ちゃんと表示されました。
とにかく、これでアプリは端末の位置情報サービスの有無
をチェックするようになりましたね。これで一件落着!と思いますが、ここから更に想定して実装していかなければいけません。
例えば、ユーザーがアラートでOKを選択して位置情報サービスをオフ
にしたままアプリに戻ったとしましょう。
どうなると思いますか?流れとしてはこんな感じ↓
位置情報サービスオフ > アプリ起動 > アラート表示(OKを選択) > 設定アプリを開く > 何もしないまま戻る > ???このままだと、アプリは
何もチェックしません
。この解決方法はアプリが
バックグラウンド状態
から戻った時に位置情報サービスの有無
をチェックすれば解決します。
では、どのようにするのかをまとめようと思いましたが、記事が長くなってしまったので後日、書こうと思います。ここまで読んで下さって、ありがとうございます!
もし、ここは違うよ!というのがありましたら気軽にコメントして下さい。
- 投稿日:2021-01-08T20:07:00+09:00
Cannot convert value of type 'DocumentSnapshot' to expected argument type 'QueryDocumentSnapshot'
QueryDocumentSnapshotからモデルをインスタンス化していたので、DocumentSnapshotから生成しようとするとエラーが出た。
DocumentSnapshotとQueryDocumentSnapshotの違い
こちらの記事が分かりやすい。
https://note.com/shion_consul/n/nd45e9f385696DocumentSnapshotは
.getDocument()
で取得する単一のデータ。ただし存在の保証はない。
QuerySnapshotはdb.collection("posts").addEventLitener()
などで取得してきたドキュメントの塊で、その一つ一つがQueryDocumentSnapshotである。QueryDocumentSnapshotはQueySnapshotの子なので存在の保証がされているっぽい。本題
私のケースですとQueryDocumentSnapshotを使ってModelからインスタンスを生成していたのですが、QueryDocumentSnapshotはDocumentSnapshotを継承したクラスだったので以下のようにするとエラーは消えました。
struct Post { var name: String? init(document: DocumentSnapshot) { guard let data = document.data() else { return } // ここはあまり良くない書き方だと思う if let name = data["name"] as? String { self.name = name } } }
- 投稿日:2021-01-08T19:54:39+09:00
【Xcode12.3】Appleプラットフォームのアプリ開発入門【SwiftUI】<1/8作成中>
SwiftUI とは
SwiftUI
とは、AppleプラットフォームのアプリケーションUIを、開発言語をSwift
とした宣言型シンタックス(Declarative Syntax)
で構築するUIフレームワーク
です。
SwiftUI
同様、Apple製品のアプリケーションUIを構築するフレームワークとしてはFlutter
が有名ですが、その違いを以下の表にまとめました。
SwiftUI Flutter 対応プラットフォーム Appleのみ iOS, Android, Web IDE Xcode Android Studio, VS code 記述形式 宣言的シンタックス 宣言的シンタックス 開発言語 Swift Dart, Widget コード量 少ない 多い 拡張性 Flutterに劣る 高い Appleプラットフォームのアプリ開発にあたって、Apple公式のフレームワーク
SwiftUI
に注目していきます。SwiftUI
オブジェクト
システムボタン(Button)
ボタンButton(action: <実行メソッド>) { <ラベル> }カラーピッカー(Color Picker)
カラーピッカーColorPicker("<ラベル>", selection: .constant(.<既定色>))日付ピッカー(Date Picker)
日付ピッカーDatePicker(selection: .constant(Date()), label: { Text("<ラベル>") })グループ(Disclosure Group)
グループDisclosureGroup("<ラベル>") { <コンテンツ> }エディタボタン
エディタボタンEditButton()
- 投稿日:2021-01-08T19:54:39+09:00
【Xcode12.3】Appleプラットフォームのアプリ開発入門【SwiftUI】<1/9作成中>
SwiftUI とは
SwiftUI
とは、AppleプラットフォームのアプリケーションUIを、開発言語をSwift
とした宣言型シンタックス(Declarative Syntax)
で構築するUIフレームワーク
です。
SwiftUI
同様、Apple製品のアプリケーションUIを構築するフレームワークとしてはFlutter
が有名ですが、その違いを以下の表にまとめました。
SwiftUI Flutter 対応プラットフォーム Appleのみ iOS, Android, Web IDE Xcode Android Studio, VS code 記述形式 宣言的シンタックス 宣言的シンタックス 開発言語 Swift Dart, Widget コード量 少ない 多い 拡張性 Flutterに劣る 高い Appleプラットフォームのアプリ開発にあたって、Apple公式のフレームワーク
SwiftUI
に注目していきます。SwiftUI
オブジェクト
システムボタン(Button)
ボタンButton(action: <実行メソッド>) { <ラベル> }カラーピッカー(Color Picker)
カラーピッカーColorPicker("<ラベル>", selection: .constant(.<既定色>))日付ピッカー(Date Picker)
日付ピッカーDatePicker(selection: .constant(Date()), label: { Text("<ラベル>") })グループ(Disclosure Group)
グループDisclosureGroup("<ラベル>") { <コンテンツ> }エディタボタン
エディタボタンEditButton()フォーム(Form)
フォームForm { <コンテンツ> }グループボックス(Group Box)
グループボックスGroupBox(label: <ラベル>) { <コンテンツ> }ラベル(Label)
ラベルLabel("<ラベル>", systemImage: "<記号>")リンク(Link)
リンクLink(destination: <URL>) { Text("<ラベル>") }リスト(List)
リストList { <コンテンツ> }ナビゲーションビュー(Navigation View), ナビゲーションリンク(Navigation Link)
ナビゲーションリンクNavigationView { //NavigationLinkはNavigationViewのスコープ内で記述 NavigationLink(destination: <Viewプロトコルに準拠したインスタンス>) { <ラベル> } }アウトライン(Outline Group)
アウトライン(実装が難解だったため、ソースコードを記述)struct CityData: Identifiable { // OutlineGroupで使用するデータはIdentifiableプロトコルに適合していなければならない //構造体"CityData"のプロパティはid, name, sitesの3つ let id = UUID() //UUID(ユニークID)…128bitの16進数 let name: String var sites: [CityData]? // タイプメソッドresult()によって値が後から代入される static func result() -> [CityData] { // タイプメソッド(静的メソッド)の定義はstaticキーワードを付加 let city1 = [CityData(name: "Site A"), CityData(name: "Site B")] let city2 = [CityData(name: "Site C"), CityData(name: "Site D")] return [CityData(name: "city1", sites: city1), CityData(name: "city1", sites: city1)] } // タイプメソッドによってsitesの値が以下のようになる // [name: city1, sites: Optional([name: site A, sites: nil], [name: Site B, sites: nil]) // name: city2, sites: Optional([name: site C, sites: nil], [name: Site D, sites: nil])] } struct ContentView: View { var body: some View { List { ForEach(CityData.result()) { city in // CityData.sitesのname: city1, city2のそれぞれに対して以下の処理を実行 OutlineGroup(city, children: \.sites) { site in // CityData.sites.sitesのname: site A, site Bのそれぞれに対して以下の処理を実行 Text(site.name) } } } } }Identifiableプロトコルpublic protocol Identifiable { associatedtype ID: Hashable //型パラメータ(=プレースホルダ)"ID"は"Hashable"プロトコルに適合 var id: Self.ID { get } //プロパティ"id"は読み取り専用であり、インスタンス自身の型パラメータ"ID"に適合 }Hashableプロトコルpublic protocol Hashable{ func hash(into hasher: inout Hasher) var hashValue: Int { get }
- 投稿日:2021-01-08T15:47:27+09:00
SwiftのTableViewの基本の基
SwiftのTableViewの基本の基
非エンジニアで最近Swiftを勉強している友人に「TableViewって何??」と質問された際を想定し、TableViewの基本についてまとめます。
※本記事では実装方法ではなく、各知識・文言などについてまとめています。
※主に自身の毎日の復習・学習の機会創出、アウトプットによる知識の定着を目的としております。暖かい目で読んで頂けますと幸いです。TableViewとは
正式にはUITableViewクラス。
UITableViewはアイテム(Cell)を行ごとに表示する機能です。よく、Todoアプリやニュースアプリなどで、同じようなUIでそれぞれの内容(写真・文言・メッセージ)が異なる情報が上から下まで並んでおりスクロールしてみるような機能があるかと思います。
その表示する機能として用いられているのがTableViewになります。一番イメージしやすい例:ツイッターやインスタなどのSNS、〇〇ニュースなどのニュースアプリ
※ちなみにUITableViewは縦スクロールだけ。横スクロールは別のクラス
UITableViewクラスは縦スクロールだけです。
※私の認識が間違っていたらすみません。ですので、よくライブ配信系のアプリなどで見る、1画面内に縦スクロールと別に横スクロールで配信者一覧を見れる状態の者はCollectionViewを用いていることが多くあります。
※CollectionViewのまとめについては後日
UITableViewのCellについて
UITableViewの中の一つ一つ(行)はUITableViewCellクラスで出来ています。
UITableView上にUITableViewCellをズラーと表示することとなります。UITableViewCellの位置:sectionとrowについて
UITableViewの中でも、よく区切られて表示されている場面があると思います。
例えば、設定画面では「規約関連」「サービスについて」「その他」などのそのくくりのタイトルがあって、その中に選択肢となるCellがあるようなUIはよく見るかと思います。その構成で指定するのがsectionとrowです。
section
一言で言うと「rowをまとめたグループ」です。
先ほどの例で言う 【「規約関連」「サービスについて」「その他」】の大きなくくりについてです。
1sectionの中に複数のrow(cell)が入ることになります。row
一言で言うと「1行(=1cell)」です。
rowが最小単位で1rowにつき、1cell配置されます。例
・SNSなどで区切りなくずらーと表示されるのはsection:1,cell:n個の構成です。
・section:3で各cell:5で作成すると、合計15cellできることになります。
※プログラムで各sectionごとのcellの個数は指定できます。UITableViewのデリゲート・プロトコル
UIViewController内に以下のデリゲートをセットします。
delegate
・UITableViewDelegateプロトコル
UITableViewで発生するイベントを処理するためのデリゲートを設定するプロパティdataSource
・UITableViewDataSourceプロトコル
UITableViewで表示するデータを供給するためのプロパティ※私はよくコードを入力して起動すると、「tableviewが動かない」「データが表示されない」場合があるのですが、大体デリゲートを設定し忘れています。これはそれぞれのデリゲートがこのような働きをになっているからですね。
※プロトコル:クラスや構造体が実装するプロパティとメソッドを定義する機能。UIViewControllerではプロトコルを実装して、プロトコル内に定義されているプロパティとメソッドを必ず実装する必要がある。
デリゲートメソッド
※基本的に利用するものだけ説明します。他にもありますので、気になる方はこちらの記事が非常に参考になると思います。
UITableViewのデリゲートメソッドまとめ※tableviewは「numberOfRowsInSection」「..cellForRowAt..」の二つのメソッドがあれば最低限動作します!
numberOfRowsInSection
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 //Int }numberOfRowsInSectionの言葉通り、セクション内のrow(=cell)の数を指定します。
numberOfSections
func numberOfSections(in tableView: UITableView) -> Int { return 1 }numberOfSectionsの文字の通り、セクションの個数を設定します。
cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) // カスタムセルの場合はwithIdentifierなど変更 return cell }cellForRowAtの文字通り、各rowごとにcellを指定します。
具体的にはcellのオブジェクトを作成し、戻り値に設定します。rowごとに設定できるため、rowやsctionを条件にif・swichなどで戻り値となるcellのオブジェクトを動的に変えることができます。
didSelectRowAt
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { }文字通り、tableviewがタップされた際の処理を記載します。
IndexPathからタップされたsectionとrowを取得できますので、それを元に、処理内容を動的に決めることができます。willDeselectRowAt
func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? { return indexPath }cellがアンタップにされた際にどのcellを非選択状態にするか指定します。
didDeselectRowAt
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { }cellが非選択状態にされた際に呼び出されます
- 投稿日:2021-01-08T13:49:23+09:00
String.addingPercentEncoding(withAllowedCharacters:) の引数
String.addingPercentEncoding(withAllowedCharacters:) が何をエンコードするか気になったので調べてみました。
ついでに URLComponents の queryItems の value 部分と、JavaScriptCore の encodeURI および encodeURIComponent とも比較してみました。悩ましい。
ちなみに、記号は全部エンコードしちゃおうと思って .alphanumerics を使うと、アクセント符号付きのアルファベット (é など) やいわゆる全角英数字がエンコードされないという罠があります。
CharacterSet ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~ urlFragmentAllowed ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ urlHostAllowed ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ urlPasswordAllowed ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ urlPathAllowed ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ urlQueryAllowed ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ urlUserAllowed ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ URLComponents ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ encodeURI ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ encodeURIComponent ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ ✅ import Foundation import JavaScriptCore let asciiSymbols = (0x20...0x7e) .map { Unicode.Scalar($0) } .filter { !CharacterSet.alphanumerics.contains($0) } let allowdChars = { (str: String) -> String in let regexp = try! NSRegularExpression(pattern: "%[0-9A-F][0-9A-F]") let range = NSRange(str.startIndex..., in: str) return regexp.stringByReplacingMatches(in: str, range: range, withTemplate: "") } let urlComponents = { () -> String in var comp = URLComponents(string: "https://example.com/")! comp.queryItems = [URLQueryItem(name: "q", value: asciiSymbols.map { String($0) }.joined())] return allowdChars(comp.url!.absoluteString.components(separatedBy: "=")[1]) }() let context = JSContext()! context.evaluateScript("var ascii = ''; for (var i = 0x20; i < 0x7f; i++) ascii += String.fromCharCode(i)") context.evaluateScript("var result = encodeURI(ascii)") let encodeURI = allowdChars(context.objectForKeyedSubscript("result")!.toString()!) context.evaluateScript("var result = encodeURIComponent(ascii)") let encodeURIComponent = allowdChars(context.objectForKeyedSubscript("result")!.toString()!) let charsets: [(String, CharacterSet)] = [ ("urlFragmentAllowed", .urlFragmentAllowed), ("urlHostAllowed", .urlHostAllowed), ("urlPasswordAllowed", .urlPasswordAllowed), ("urlPathAllowed", .urlPathAllowed), ("urlQueryAllowed", .urlQueryAllowed), ("urlUserAllowed", .urlUserAllowed), ("URLComponents", CharacterSet(charactersIn: urlComponents)), ("encodeURI", CharacterSet(charactersIn: encodeURI)), ("encodeURIComponent", CharacterSet(charactersIn: encodeURIComponent)), ] print("|CharacterSet|" + asciiSymbols.map { String($0) }.map { ($0 == "|" ? "|" : $0) + "|" }.joined()) print("|:--|" + asciiSymbols.map { _ in ":-:|" }.joined()) for cs in charsets { print("|\(cs.0)|" + asciiSymbols.map { (cs.1.contains($0) ? "" : "✅") + "|" }.joined()) }