- 投稿日:2020-09-11T23:28:58+09:00
RxSwift における subscribeOn と observeOn の違い
RxSwift などで処理をつなげて書いていると、ここはメインスレッドで、ここはバックグランドスレッドでなどと、処理によってスレッドを切り替えたい場合があるかと思います。そんな時は、
Observable
のプロパティとして提供されているsubscribeOn
または、observeOn
を使用することでスレッドを切り替えて処理を実行することができます。今回はそんなsubscribeOn
とobserveOn
の違いについて簡単にまとめていこうかと思います。その前に Scheduler
RxSwift では Observable や subscribe の処理をどのスレッドでどのよう(直列・並列)に処理をさばくかを決定する役割を
Scheduler
と呼びます。これについては後で触れますが、RxSwift を使用し、マルチスレッドで処理を行う際にはとても重要な知識です。subscribeOn
subscribeOn
は参照するストリームの元となる Observable の実行スレッドとそれに続くメソッドのスレッドを決定します。SerialDispatchQueueScheduler
で Scheduler を生成し、バックグランドスレッドの直列処理で実行したサンプルが下記のようになります(呼び出しはメインスレッド)。どのように出力されるか結果を見る前に予想してみてください??let ob = Observable<Int>.create { observer -> Disposable in for i in 0...10000 { if i == 10000 { print("observer called!") observer.onNext(1) observer.onCompleted() } } return Disposables.create() } let scheduler = SerialDispatchQueueScheduler(qos: .background) ob.subscribeOn(scheduler) .subscribe(onNext: { _ in print(1) for i in 0...100 { if i == 100 { print(2) } } }) .disposed(by: disposeBag) ob.subscribeOn(scheduler) .subscribe(onNext: { _ in print(3) }) .disposed(by: disposeBag)出力
// 全てバックグランドスレッド observer called! 1 2 observer called! 3上から順番にバックグランドスレッドにタスクが追加されていき、直列でそれぞれの処理が実行されているのが分かります。また、基本的には、同じスレッドで実行したい場合には
Scheduler
のインスタンスは統一する必要があるかと思います。(試しに同じSchedler
を別々のインスタンスで実行したところ直列で実行がなされませんでした?原因がわかる人教えください?♂️)observeOn
observeOn
はどのScheduler
で次の Observer にイベントを流すのかを決定します。MainScheduler.instance
でSchedler
を生成して、購読部分のみメインスレッドで行ったサンプルは下記のようになります。DispatchQueue.global(qos: .background).async { let ob = Observable<Int>.create { observer -> Disposable in for i in 0...10000 { if i == 10000 { print("observer called!") observer.onNext(1) observer.onCompleted() } } return Disposables.create() } ob.observeOn(MainScheduler.instance) .subscribe(onNext: { _ in print(1) for i in 0...100 { if i == 100 { print(2) } } }) .disposed(by: self.disposeBag) ob.observeOn(MainScheduler.instance) .subscribe(onNext: { _ in print(3) }) .disposed(by: self.disposeBag) }出力
observer called! // バックグランドスレッド 1 // メインスレッド 2 // メインスレッド observer called! // バックグランドスレッド 3 // メインスレッドこちらはバックグランドの処理により、全体の順番が変わることはありますが、基本的にはメインスレッドでの処理は直列処理なので、順番が変わることはありません。
Scheduler の種類
先でもスレッドや処理の実行順などを指定するのに
Schedler
を使用しましたが、RxSwift では現在下記のようなScheduler
が用意されています。
Class 内容 CurrentThreadScheduler (Serial scheduler) Observable.create などで指定されるデフォルトのスケジューラにはこれがしてされていて、スレッドを切り替えず現在のスレッドで処理を実行します。 MainScheduler (Serial scheduler) これは Swift の UI などを更新するためのデフォルトのスレッドで、このスケジューラはメインメソッドから呼ばれた場合はスケジューリングなしですぐに処理が実行されます SerialDispatchQueueScheduler (Serial scheduler) バックグランドで処理を直列に実行すれ場合に使用するスケジューラで、Qos を引数にとり最適なスレッドを選択することができます。 ConcurrentDispatchQueueScheduler (Concurrent scheduler) バックグランドで処理を並列に実行する場合に使用するスケジューラで、 SerialDispatchQueueScheduler
と同様に Qos を指定して使用できます。OperationQueueScheduler (Concurrent scheduler) NSOperationQueue
を使った非同期処理を行う時に使用します。また、maxConcurrentOperationCount
などで同時実行数を制御したい時などに便利なスケジューラです。https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md
参考
- https://github.com/ReactiveX/RxSwift/blob/master/Documentation/Schedulers.md
- https://qiita.com/k5n/items/e80ab6bff4bbb170122d
- http://reactivex.io/documentation/scheduler.html
- https://medium.com/eureka-engineering/rxswift%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%9E%E3%83%AB%E3%83%81%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89%E3%81%AE%E7%90%86%E8%A7%A3%E3%82%92%E6%B7%B1%E3%82%81%E3%82%8B-scheduler%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6-2471ec76e518
- 投稿日:2020-09-11T21:54:05+09:00
テストについて学習してみた
- 投稿日:2020-09-11T15:23:31+09:00
tableView画面クラッシュ
tableViewの画面がクラッシュする
【エラー】
Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread.【対応】
テーブルビューのリロード部分
self.rankingTableView.reloadData()
をDispatchQueue.main.async {
self.rankingTableView.reloadData()
}と囲むとクラッシュしなくなりました。
【参考】
swift初心者がiOS13対応でメインスレッド以外でUI更新をしてクラッシュさせてしまった話
https://qiita.com/rymiyamoto/items/7ace750172b84a2ff809
- 投稿日:2020-09-11T11:10:57+09:00
APIを利用したアプリをMVCで書く際につまずいたところ(Swift)
はじめに
今回僕は、位置情報から国土地理院のAPIを使って標高を取得し、表示するという簡単なアプリをMVCで書こうと思った際にあるところでつまずきました。MVCで書こうと思ったのはなんとなくかっこいいし、きれいだから。
具体的に言うと、ModelファイルでAPIを利用して取得した標高をControllerファイルに渡す際につまずきました。非同期通信らへんの問題です。どうしてもうまく行かなくて、teratailで質問してみたところ親切な人に丁寧な回答をもらい解決することができました。ありがとうございました。
回答にはクロージャを使っていました。初心者の敵クロージャです。しかし今回始めてそのクロージャの使い道と便利さがわかったような気がしたので、よかったら最後まで見てください。
大まかな流れ
Modelで標高を取得し、それをControllerを通じてViewにわたすという操作をしています
Controller//位置情報が更新されたときに呼ばれるメソッド func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { DispatchQueue.main.async {//UIの更新はメインスレッドで行う //位置情報が入ったlocationsをModelに渡して標高を取得し //その情報をViewに渡してUIを更新する customView.elevation = model.getElevation(locations) } }Modelvar elevation = 0.0 func getElevation(_ Locations: [CLLocation]) -> Double { //通信処理の準備 APIを使った通信処理 { (data, response, error ) in //デコードしたりなんやかんやで標高を取得する elevation = 取得した標高 } //取得した標高を返す return elevation }つまずきポイント
どうやってもgetElevationで取得した標高をControllerを通じてViewにわたすことができない。
非同期通信についてたくさん調べてやってみたが全然うまく行かない。通信処理からelevationに標高を代入する前にgetElevationメソッドが終了してしまい、取得した標高をUIに表示することができませんでした。そんなときに助けてくれたのがクロージャでした。ここで役に立つのかクロージャ!
以前クロージャの記事を書いたが、いまいち使い所がわからなかった。
Swiftクロージャ これいつどうやって使うの?
ところが今回なんとなくわかった、気がする。まず今回うまく行かなかった原因は、取得した標高を変数に代入する前にメソッドが終了して、意図した戻り値を返すことができていなかったからです。
これを解決するためにreturnを使うのをやめ、通信処理が完了したらクロージャを実行して値を返すようにします。
クロージャの出番
今までのコードを以下のように書き換えます
Controllerfunc locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { model.getElevation(locations, completionHandler: { (elevationString) in DispatchQueue.main.async { self.customView.elevation = elevationString ?? "標高を取得できませんでした" } }) }Modelvar elevationString = "0.0m" func getElevation(_ Locations: [CLLocation], completionHandler: @escaping (String?) -> Void) { //通信処理の準備 APIを使った通信処理 { (data, response, error) in //デコードしたりなんやかんやで標高を取得 elevationString = 取得した標高 completionHandler(elevationString)解説
最初にこの書き方を見たときは少し怯んだのですが、自分なりにコードの解説をしてみます。
①クロージャの設置
まずクロージャを使ってcompletionHandlerというものを定義しています。このcompletionHandlerはString?型の変数を持っています。
②クロージャに値を代入
国土地理院から標高を取得するという通信処理が完了したら、クロージャを呼び出してもらってその標高を代入します。
③代入された値を利用する
代入された値を利用して、Viewに渡します。
実は③の部分は最初から使っていた以下のコードと同じ操作をしています。
ModelAPIを使った通信処理 { (data, response, error) in //dataを解析したり //errorが返ってきたら出力したり }今回だと通信処理が完了するまではクロージャは呼び出されず、何も値を持っていません。通信処理が完了したらクロージャが呼び出され値を代入し、その値をViewに渡すというふうになっています。
まとめ
まだまだ理解しきれていないところはたくさんあります。
@escaping
について- 循環参照について
クロージャの使い所を実感できただけでも成長したなと思いました。修正やもっとこんなふうに書いたほうがいいよなどありましたらお願いします。
- 投稿日:2020-09-11T06:16:07+09:00
iOS 14:クリップボードにアクセスする前にその内容の種別を検知
iOS 14では、アプリがユーザーのクリップボードのコンテンツにアクセスするたびに、画面上部に警告が表示されます。
アプリがクリップボードのコンテンツにアクセスするのは、その中のコンテンツに気になる点があるかどうか知りたい場合があるためです。
この
UIPasteboard.general
のdetectPatterns
機能を利用すると、クリップボードのコンテンツが次のカテゴリのいずれかに該当するかどうか検出することができます。
- 数字
.number
- URL
.probableWebURL
- ウェブ検索クエリー
.probableWebSearch
detectPatterns
がコンテンツのタイプを判断できない時はいつも.probableWebSearch
と報告しているようです。完了したGithubのデモプロジェクトはこちらから入手できます。
ペーストボードのコンテンツを検出
UIPasteboard.general.detectPatterns(for: [.number, .probableWebSearch, .probableWebURL]) { result in switch result { case .success(let patterns): if patterns.contains(.number) { // おそらく数字 } else if patterns.contains(.probableWebSearch) { // おそらくウェブ検索クエリー } else if patterns.contains(.probableWebURL) { // おそらくURL } case .failure(let error): print(error.localizedDescription) } }
UIPasteboard.general.detectPatterns
を使用すると、アラートが表示されません。興味のあるコンテンツがペーストボードに含まれていることがわかったら、実際のペーストボードのコンテンツにアクセスできます。(たとえば、お使いのアプリが、ペーストボードからURLをコピーすることだけに興味がある場合、まず検出を実行して、URLがペーストボードに含まれていたらペーストボードへアクセスできます)
デモアプリケーション
こちらがGithub上でどのように機能するのかをより良く把握していただけるよう、デモアプリケーションのダウンロードが可能です。
デモアプリケーション内には2つのボタンがあり、
パターンを検出
のボタンではペーストボードの内容のタイプが表示され、クリップボードの内容を取得
のボタンではペーストボードの内容がフェッチされます。⭐️ こちらのウェブページにアクセスすると、私の公開されているQiitaの記事のリストをカテゴリー別にご覧いただけます。
- 投稿日:2020-09-11T01:07:20+09:00
【Swift】AlamofireでURLRequestValidationFailureReason.bodyDataInGETRequest(14 bytes)が出た
AlamofireでAPI周りを実装した時に以下のエラーが出ました。
Alamofire.AFError.urlRequestValidationFailed(reason: Alamofire.AFError.URLRequestValidationFailureReason.bodyDataInGETRequest(14 bytes))
どうやらエンコーディングの方法が違いエラーをはいているようです。
この時に指定していたエンコーディングはJSONEncoding.default
でした。
これをURLEncoding.default
に変更することでリクエストは正常に処理されました。結果的に問題は解決出来たのですが、理由は分からず。。
ちょっと前までJSONEncoding.default
で正常に動作していた気がするのですが、何か知見のある方コメント頂けると嬉しいです?