- 投稿日:2020-02-13T19:33:34+09:00
Swiftでドローンを飛ばしてみる
Swiftでドローンを飛ばしてみる
この記事はSwift愛好会vol49@JapanTaxiでの発表の詳細の記事です。
発表のスライドはこちらです。
上記のスライドにもあるドローンを使ったエンタメに興味をもち、また愛好会の発表のネタにもなりそうだったので、Telloのドローンを買って、遊んでみました。仕組み
下の図のように、iPhoneなどの端末とドローンをUDP通信で接続します。その後指定のコマンドを送ることで、ドローンがそのコマンド通りに動いてくれます。
実装
@eito_2さんのこちらの記事を参考にし、Network FrameworkのNWNetworkを使用して、実装しました。WWDC2019でそれに関する動画がありましたので、興味のある方はどうぞ。
まずはドローンのアドレス/ポート情報をまとめた構造体とコマンド一覧を定義したenumを準備します。コマンドは一部しか定義していないので、利用可能なコマンド一覧をみたい方は、SDK2.0 User Guideをご確認ください。struct TelloConstants { static let ipAddress = "192.168.10.1" static let port: UInt16 = 8889 } enum TelloCommands { case start case takeoff case land case emergency // Min: 20, Max: 500(cm) case left(x: Int) case right(x: Int) case forward(x: Int) case back(x: Int) case up(x: Int) case down(x: Int) // Min: 1, Max: 360(degree) case rotateRight(x: Int) case rotateLeft(x: Int) case flip(direction: TelloDirection) } extension TelloCommands { var asString: String { switch self { case .start: return "command" case .land: return "land" case .takeoff: return "takeoff" case .emergency: return "emergency" case .left(let x): return "left \(x)" case .right(let x): return "right \(x)" case .forward(let x): return "forward \(x)" case .back(let x): return "back \(x)" case .up(let x): return "up \(x)" case .down(let x): return "down \(x)" case .rotateRight(let x): return "cw \(x)" case .rotateLeft(let x): return "ccw \(x)" case .flip(let direction): return "flip \(direction.asString)" } } } enum TelloDirection { case left case right case forward case back var asString: String { switch self { case .left: return "l" case .right: return "r" case .forward: return "f" case .back: return "b" } } }次にドローンとの接続と実際にコマンドを送信する実装です。
init時に、stateUpdateHandler
にクロージャーをセットすることで、ドローンとの接続状況を確認できるようにしています。
またメソッドとしては、以下の3つを用意しました。
1.コマンドのenumを受け取り、コマンド実行後のレスポンスを受信後、指定のクロージャにResult型で結果を通知する。
2.コマンドを受け取り、その実行結果をSingleで返す(1のラッパーメソッド)。
3.コマンドの配列とコマンド実行間隔の秒数を受け取り、そのコマンドを順に実行し、その実行結果をObservableで返す。class TelloManager { let connection = NWConnection(host: .init(TelloConstants.ipAddress), port: .init(integerLiteral: TelloConstants.port), using: .udp) init() { connection.stateUpdateHandler = { [unowned self] state in switch state { case .setup: print("Setup") case .waiting(let error): print(error) case .preparing: print("preparing") case .ready: self.connection.start(queue: .global()) print("ready") case .failed(let error): print(error) case .cancelled: print("cancelled") @unknown default: fatalError("Unknown error") } } } //1.コマンドのenumを受け取り、コマンド実行後のレスポンスを受信後、指定のクロージャにResult型で結果を通知する。 func send(command: TelloCommands, completion: @escaping ((Result<String, Error>) -> Void)) { print("Commands: ", command.asString) let message = command.asString.data(using: .utf8) connection.send(content: message, contentContext: .defaultMessage, isComplete: true, completion: .contentProcessed({ error in if let error = error { completion(.failure(error)) } })) connection.receive(minimumIncompleteLength: 0, maximumLength: Int(Int32.max)) { (data, context, isComplete, error) in if !isComplete, let error = error { completion(.failure(error)) } else if let data = data, let message = String(data: data, encoding: .utf8) { completion(.success(message)) } else { print("Unknown error") } } } //2.コマンドを受け取り、その実行結果をSingleで返す(1のラッパーメソッド)。 func send(command: TelloCommands) -> Single<String> { return Single<String>.create { [unowned self] single -> Disposable in self.send(command: command) { result in switch result { case .success(let message): single(.success(message)) case .failure(let error): single(.error(error)) } } return Disposables.create() } } //3.コマンドの配列とコマンド実行間隔の秒数を受け取り、そのコマンドを順に実行し、その実行結果をObservableで返す。 func sends(parameters: (commands: [TelloCommands], interval: Int)) -> Observable<String> { let commandsObservables = parameters.commands.map(send) .map({ $0.asObservable().delay(.seconds(parameters.interval), scheduler: MainScheduler.instance) }) return Observable.concat(commandsObservables) } }ViewControllerの実装は以下の通りです。
このドローンの制約上、最初にcommand
というコマンドを最初に送信しないといけないので、setup
というボタンをタップしてから、その後の操作に入るようになっています。その後takeoff
ボタンをタップしたら離陸、land
ボタンを押したら着陸、pattern1
を押したら、2秒間隔で「離陸→60cm上昇→30cm下降→左に100cm移動→右に100cm移動→前にフリップ→後ろにフリップ→着陸」が実行されます。ちなみにフリップはこんな動きをします。class ViewController: UIViewController { @IBOutlet weak var setupButton: UIButton! @IBOutlet weak var takeOffButton: UIButton! @IBOutlet weak var landButton: UIButton! @IBOutlet weak var rightButton: UIButton! @IBOutlet weak var leftButton: UIButton! @IBOutlet weak var pattern1Button: UIButton! private let resultRelay = PublishRelay<String>() private let disposeBag = DisposeBag() private let tello = TelloManager() override func viewDidLoad() { super.viewDidLoad() setupButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance) .map({ _ in .start }) .flatMap(tello.send) .bind(to: resultRelay) .disposed(by: disposeBag) takeOffButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance) .map({ _ in .takeoff }) .flatMap(tello.send) .bind(to: resultRelay) .disposed(by: disposeBag) landButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance) .map({ _ in .land }) .flatMap(tello.send) .bind(to: resultRelay) .disposed(by: disposeBag) rightButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance) .map({ _ in .right(x: 100) }) .flatMap(tello.send) .bind(to: resultRelay) .disposed(by: disposeBag) leftButton.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance) .map({ _ in .left(x: 100) }) .flatMap(tello.send) .bind(to: resultRelay) .disposed(by: disposeBag) pattern1Button.rx.tap.throttle(.seconds(1), scheduler: MainScheduler.instance) .map({ _ -> [TelloCommands] in [.takeoff, .up(x: 60), .down(x: 30), .left(x: 100), .right(x: 100), .flip(direction: .forward), .flip(direction: .back), .land] }) .map({ ($0, 2) }) .flatMap(tello.sends) .bind(to: resultRelay) .disposed(by: disposeBag) resultRelay.asObservable() .subscribe(onNext: { message in print("Message: ", message) }, onError: { (error) in print("Error: ", error) }) .disposed(by: disposeBag) } }まとめ
普段はモバイル内のみを動かすことが多いSwiftですが、ドローンを動かせたのは新鮮でした。
Try!Swift2019ではSwiftでラズパイを動かす発表もあったので、いずれそちらも触ってみたいです。
またこのドローンはカメラの情報も受け取れる仕組みになっているので、その情報を受け取って、写真や動画の撮影できるようにするのも、いずれ実装したいです(今回は時間がなくて、断念しました)。
ソースコードはこちらです。参考
•https://www.ryzerobotics.com/jp/tello
•https://qiita.com/eito_2/items/0d9e2c92b0be0ea16e77
•https://dl-cdn.ryzerobotics.com/downloads/Tello/Tello%20SDK%202.0%20User%20Guide.pdf
- 投稿日:2020-02-13T16:54:04+09:00
Firebase Authenticationを学習しよう(Facebookログイン編)
完成予想図
はじめに
FirebaseでSNSユーザー認証をしたい時に参考にしてください!
メールアドレスとパスワードで認証するノーマルバージョンを試したい場合は、
https://firebase.google.com/docs/auth/ios/start?hl=ja(公式ドキュメント)
と
https://dev.classmethod.jp/smartphone/iphone/firebase-authentication-ios/
がわかりやすいです!今回は、facebookログインをやってみます!
Firebase Authenticationとは
Firebase Authentication は、 Google のフェデレーション認証で、 Google 以外にも Facebook 、 GitHub 、 Twitter などのサードパーティの認証情報を使用して、クライアントアプリケーションのログインを行うことができます。また、フェデレーション ID 以外にもパスワード認証や電話番号認証にも対応しています。
※フェデレーションとは...一度認証を通れば、その認証情報を使って、許可されているすべてのサービスを使えるようにする仕組みのこと。
引用元
https://www.topgate.co.jp/firebase01-what-is-firebaseインポート
podfileを作成後、
firebaseやfacebook関連のものをファイルに取り込みましょう!pod 'Firebase/Auth' pod 'FacebookSDK' pod 'Firebase/Core' pod 'FacebookCore' pod 'FacebookLogin' pod 'FacebookShare' pod 'SDWebImage/WebP'Firebaseの設定
https://qiita.com/ShinokiRyosei/items/f71c73ab8b0de145c5bc
ここに設定手順が記載されているので参考にしてください!https://qiita.com/Ritsuya/items/0ad42ae40337ae36f472
Firebase全般に関してわからない方はこちらを参照してみてください!プロジェクトファイルに「GoogleService-Info」 のファイルを入れるときの注意点です!
①「Copy items if needed」にチェックが入ってること
②「GoogleService-Info(1)」となっている取り入れたファイル名をxcode内で「GoogleService-Info」に変更しましょうを確認してファイルに取り入れましょう!
Facebookログインの設定
まず、Firebase上でFacebookログインを有効にします!
右上で有効にした後に、アプリケーションIDとアプリシークレットを入力します!
これらは、facebook developerのサイトの番号を入力します。
右上のマイアプリ=>アプリの作成
その次に出てくる、表示名の項目は自由に決めてオッケーです!(ただしfacebookというワードはNGのようです)プライバシーポリシーやカテゴリも決めておきましょう!
そして、これらの情報に対応する部分を先ほどのfirebase(アプリケーションIDとアプリシークレット)に登録しましょう!次に、左側のプロダクトを追加します!
その中のFacebookログインの設定を行います。次に、左側の設定の有効なOAuthリダイレクトURIというところに、firebaseのfacebookの部分のURLを載っけます。(写真赤枠)
最後に、ここまで設定すれば、上のステータスをオンにすることができます!
オンに切り替えてください。FirebaseとFacebookの導入
AppDelegate.swiftimport Firebase import FBSDKCoreKit //FacebookSDK func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { //追加 FirebaseApp.configure() //Facebookログイン ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions) return true } //追加 func application(_ application : UIApplication,open url: URL, sourceApplication: String?, annotation: Any)->Bool{ return ApplicationDelegate.shared.application(application, open: url, sourceApplication: sourceApplication, annotation: annotation) } //追加 func applicationDidBecomeActive(_ application: UIApplication) { AppEvents.activateApp() }info.plistの設定
info.plistに以下の項目を追加し、facebook developerのサイトで設定したIDを赤い部分に入力しましょう
FirebaseDisplayNameも、facebookDeveloperのサイトで設定したプロダクト名を入力してください。Facebookログインボタンの表示
コードでボタンを配置します!
①必要なものをインポート
②ボタンをコード上で配置
③ログインボタンのプロトコル宣言をし、delegateを設定
④facebookログインに必要な関数をかくimport UIKit import FBSDKCoreKit import FBSDKLoginKit import FacebookCore import FacebookLogin import Firebase class ViewController: UIViewController , LoginButtonDelegate{ let fbLoginButton: FBLoginButton = FBLoginButton() var displayName = String() var pictureURL = String() var pictureURLString = String() override func viewDidLoad() { super.viewDidLoad() fbLoginButton.delegate = self fbLoginButton.frame = CGRect(x: view.frame.size.width / 2 - view.frame.size.width / 4, y: view.frame.size.height / 4, width: view.frame.size.width / 2, height: 30) //許可するもの fbLoginButton.permissions = ["public_profile, email"] view.addSubview(fbLoginButton) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) //ナビゲーションバーを消す navigationController?.isNavigationBarHidden = true } func loginButton(_ loginButton: FBLoginButton, didCompleteWith result: LoginManagerLoginResult?, error: Error?) { //loginする if error == nil{ if result?.isCancelled == true{ //キャンセルされた場合は何もしないで返す return } } let credential = FacebookAuthProvider.credential(withAccessToken: AccessToken.current!.tokenString) //ここからfirebase Auth.auth().signIn(with: credential) { (result, error) in if let error = error { return } self.displayName = (result?.user.displayName)! //string型に強制変換 self.pictureURLString = (result?.user.photoURL!.absoluteString)! //画像の大きさを変更(大きくした) self.pictureURLString = self.pictureURLString + "?type=large" //次の画面遷移で画像を表示させるために、userdefaultを用いて保存&ログイン保持 let ud = UserDefaults.standard ud.set(1, forKey: "loginOK") //ログイン保持 ud.set(self.pictureURLString, forKey: "pictureURLString") //次の画面の遷移先を指定 let nextVC = self.storyboard?.instantiateViewController(identifier: "next") as! NextViewController self.navigationController?.pushViewController(nextVC, animated: true) } } func loginButtonWillLogin(_ loginButton: FBLoginButton) -> Bool { return true } func loginButtonDidLogOut(_ loginButton: FBLoginButton) { //ログアウト } }最後に
次の画面遷移で画像を表示させるのは、userdefaultから値を取ってきて代入するだけです!
facebookログインは、facebook developerのサイトの設定が少し多いですがぜひやってみてください!
- 投稿日:2020-02-13T14:16:43+09:00
Swiftでの繰り返し処理
swiftの繰り返し処理
アウトプットとして、初めてQittaで投稿します。もし間違えてるとこがありましたら、指摘して下さい!
処理を繰り返す for - in文
qiita.swiftfor 変数 in 配列 { ステートメント }example.swift//配列の定義 var numList = [2, 4, 6, 8, 10] var sum = 0 for num in numList { sum += num } print("合計\(sum)") => 30ちなみに変数numは繰り返し処理のなかで定義したので、外では使えません。
for - in で指定回数繰り返す
レンジから取り出した値を使わない場合、値を受け取る変数の代わりに _ を指定する
example.swiftvar stars = "" for _ in 1...5 { stars += "★" print(stars) } =>★ ★★ ★★★ ★★★★ ★★★★★レンジとは
レンジは整数の範囲を表します。二つの違いは終了値を含むか含まないか
レンジ.swift開始値...終了値 1...5 //1 ~ 5 開始値..<終了値 1..< 5 //1 ~ 4 }最後に
初めての投稿でわかりづらいかもしれませんが、これからも投稿を続けようと思いおいます。Swift勢のみなさん頑張りましょう!ありがとうございました
- 投稿日:2020-02-13T12:09:53+09:00
[Swift5]TableViewCellをスワイプして削除する際にアラートを出す(クロージャを引数に持つ便利なアラート関数)
はじめに
セルの削除など重要な処理をする際にアラートを表示することで不本意な操作を避けることができます。
この記事では使い回しができるアラート関数を実装し、それを利用してアラートを表示していきたいと思います。使い回しができるアラート関数は1つの
TableView
を使いまわすことなどにより、関数内の記述が場合分けによって増えてしまうのを防ぐ有効な手段となります。目次
- 環境
- 実行例
- 考え方
- TableViewの実装
- アラート関数の実装
- ソースコード
- 終わりに
環境
- Xcode 11.2.1
- Swift5
実行例
考えかた
実行例のようなプログラムを実装するにあたって説明しなければならない要素が3つあります。
TableView
とSegmentControl
とアラート関数
です。
今回はアラート関数
に関する説明を重点的に行いたいので、その他の要素については軽く触れる程度にします。
また、SegmentControl
に関しては必ずしも必要なわけではないので今回は説明しませんが、とても分かりやすい記事があるので共有しておきます。TableViewの実装
はじめに、
TableView
を削除可能にするための実装の説明を少しします。
以下のコードでセルの編集許可を与えることで可能になります。func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true }またセルに対する編集が行われた場合に呼び出されるイベント関数が以下になります。
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {...}アラート関数の実装
さて本題ですね。クロージャさえ理解できればあとは簡単です。
アラート関数は以下のように表現されます。
private func alert(alertTitle: String, alertMessage: String, okTitle: String, cancelTitle: String, Closure: @escaping ((Bool) -> Void)) { var isOK = false let alert: UIAlertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: UIAlertController.Style.alert) for subview in (alert.view.subviews) { subview.backgroundColor = UIColor(red: 50/255, green: 143/255, blue: 145/255, alpha: 1) subview.layer.cornerRadius = 20 subview.alpha = 1 } // OK let defaultAction: UIAlertAction = UIAlertAction(title: okTitle, style: UIAlertAction.Style.default, handler:{ (action: UIAlertAction!) -> Void in isOK = true Closure(isOK) }) // キャンセル let cancelAction: UIAlertAction = UIAlertAction(title: cancelTitle, style: UIAlertAction.Style.cancel, handler:{ (action: UIAlertAction!) -> Void in isOK = false Closure(isOK) }) alert.addAction(cancelAction) alert.addAction(defaultAction) present(alert, animated: true, completion: nil) }引数にアラートのタイトル、アラートメッセージ、OKのタイトル、キャンセルのタイトル、操作時に呼び出す処理(クロージャ)を持っていることが分かりますね。
引数のクロージャに
@escaping
が付くのは、このクロージャがdefaultAction
とcancelAction
のクロージャで呼ばれるためです。
これは非同期処理によって引数のクロージャが解放されるのを防ぐという意味ですね。このように実装することでアラート関数を短く呼び出すことが可能となります。
では呼び出す側を見てみましょう。
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { func update(_ isOK: Bool) { if(isOK) { //省略 let indexSet = NSMutableIndexSet() indexSet.add(indexPath.section) tableView.deleteSections(indexSet as IndexSet, with: UITableView.RowAnimation.automatic) tableView.reloadData() } } switch self.mySegmentControl.selectedSegmentIndex { case 0: alert( alertTitle: "本当ニ33娘ヲ削除シマスカ?", alertMessage: "bye~(- o -)", okTitle: "スル!", cancelTitle: "シナイ", Closure: { isOK in update(isOK) }) case 1: alert( alertTitle: "本当に22娘を削除しますか?", alertMessage: "goodbye~(o w o)", okTitle: "します!", cancelTitle: "しません", Closure: { isOK in update(isOK) }) default: break } }このようにスッキリ記述することができました。
ソースコード
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { func update(_ isOK: Bool) { if(isOK) { switch mySegmentControl.selectedSegmentIndex { case 0: if let i = showndata.firstIndex(of: "33娘") { showndata.remove(at: i) } case 1: if let i = showndata.firstIndex(of: "22娘") { showndata.remove(at: i) } default: break } let indexSet = NSMutableIndexSet() indexSet.add(indexPath.section) tableView.deleteSections(indexSet as IndexSet, with: UITableView.RowAnimation.automatic) tableView.reloadData() } } switch self.mySegmentControl.selectedSegmentIndex { case 0: alert( alertTitle: "本当ニ33娘ヲ削除シマスカ?", alertMessage: "bye~(- o -)", okTitle: "スル!", cancelTitle: "シナイ", Closure: { isOK in update(isOK) }) case 1: alert( alertTitle: "本当に22娘を削除しますか?", alertMessage: "goodbye~(o w o)", okTitle: "します!", cancelTitle: "しません", Closure: { isOK in update(isOK) }) default: break } } private func alert(alertTitle: String, alertMessage: String, okTitle: String, cancelTitle: String, Closure: @escaping ((Bool) -> Void)) { var isOK = false let alert: UIAlertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: UIAlertController.Style.alert) for subview in (alert.view.subviews) { subview.backgroundColor = UIColor(red: 50/255, green: 143/255, blue: 145/255, alpha: 1) subview.layer.cornerRadius = 20 subview.alpha = 1 } // OK let defaultAction: UIAlertAction = UIAlertAction(title: okTitle, style: UIAlertAction.Style.default, handler:{ (action: UIAlertAction!) -> Void in isOK = true Closure(isOK) }) // キャンセル let cancelAction: UIAlertAction = UIAlertAction(title: cancelTitle, style: UIAlertAction.Style.cancel, handler:{ (action: UIAlertAction!) -> Void in isOK = false Closure(isOK) }) alert.addAction(cancelAction) alert.addAction(defaultAction) present(alert, animated: true, completion: nil) }終わりに
クロージャって便利ですね。
セルを削除する際のデータに対する処理が複雑になればなるほど、スッキリ記述する重要性は高まっていくと思います。
そのような時にこの記事が役に立てば幸いです。
- 投稿日:2020-02-13T12:02:06+09:00
チュートリアルでありがちなテキストが少しずつ表示されていくUILabelを実装する方法
うまく表現できませんが、チュートリアルなんかでよく、「カタカタカタカタ」と少しずつテキストが表示されていくシーンがあるじゃないですか!(伝わってください)
今回はそれを実装しました。
既存のライブラリで存在していたら教えてください少しずつテキストを表示して、表示中にタップされたらテキストを全部まとめて表示するUILabelを実装しました。
// テキストを表示中を表すフラグと、早く表示するためのフラグです fileprivate var isPlaying = false fileprivate var isFast = false fileprivate var tutorialLbl: UILable! override func viewDidLoad() { super.viewDidLoad() // タップを検知 self.singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.singleTap(_:))) self.view.addGestureRecognizer(self.singleTapGesture) } // 画面をタップした際に発火する関数 @objc func singleTap(_ sender: UITapGestureRecognizer){ // 一度タップされていてテキストがカタカタと表示中のとき、表示速度を早めるフラグを立てる if self.isPlaying { self.isFast = true return } self.isPlaying = true // このメソッドはよしなに showTutorial() } fileprivate func showTutorial() { // ラベルをテキトーに作成 self.tutorialLbl = UILabel() self.tutorialLbl.frame = CGRect(x: 0, y: self.view.bounds.maxY - 200, width: self.view.bounds.width, height: 200) self.view.addSubview(self.tutorialLbl) let text = "アイウエオかきくけこさしすせそ" // ここで表示 self.showTextDynamically(label: self.tutorialLbl, text: Array(text), idx: 0) } fileprivate func showTextDynamically(label: UILabel, text: [Character], idx: Int) { var waitTime = 0.0 // 表示速度を早めるフラグが立っていないとき、0.1秒間に一文字表示するよう待ち時間を設定 if !self.isFast { waitTime = 0.1 } // テキストを表示しきったときリターン if text.count == idx { self.isPlaying = false self.isFast = false return } if label.text == nil { label.text = "" } // 0.1秒遅延して再帰する DispatchQueue.main.asyncAfter(deadline: .now() + waitTime) { // テキストをlabelに追加する let c = text[idx] label.text! += String(c) self.showTextDynamically(label: label, text: text, idx: idx + 1) } }こういうのってtimerを使うのが一般的なんでしょうか。
- 投稿日:2020-02-13T01:06:27+09:00
Swift:WKWebViewでローカルファイルを開く時のTips
import Cocoa import WebKit class ViewController: NSViewController { var webView: WKWebView? { return self.view as? WKWebView } override func viewDidLoad() { super.viewDidLoad() guard let url = Bundle.main.url(forResource: "LocalFile", withExtension: "html") else { return } self.webView?.loadFileURL(url, allowingReadAccessTo: url) } }こんな感じでBundle内のHTMLファイルをWebViewに表示するとき、
SandBox
のNetwork: Outgoing Connections (client)
にチェックを入れないとコンテンツが読み込まれない。