20200911のSwiftに関する記事は6件です。

RxSwift における subscribeOn と observeOn の違い

RxSwift などで処理をつなげて書いていると、ここはメインスレッドで、ここはバックグランドスレッドでなどと、処理によってスレッドを切り替えたい場合があるかと思います。そんな時は、Observable のプロパティとして提供されている subscribeOn または、observeOn を使用することでスレッドを切り替えて処理を実行することができます。今回はそんな subscribeOnobserveOn の違いについて簡単にまとめていこうかと思います。

その前に 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.instanceSchedler を生成して、購読部分のみメインスレッドで行ったサンプルは下記のようになります。

    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

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テストについて学習してみた

自動テストについて学んだこと

現在、自動テストについて学習しているので学習したことをまとめていきます。

これから

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

https://www.it-swarm.dev/ja/ios/gtレイアウトエンジンへの変更は、メインスレッドからアクセスした後、バックグラウンドスレッドから実行しないでください%E3%80%82/811797116/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
    }
}
Model
var elevation = 0.0

func getElevation(_ Locations: [CLLocation]) -> Double {
    //通信処理の準備

    APIを使った通信処理 { (data, response, error ) in
        //デコードしたりなんやかんやで標高を取得する
        elevation = 取得した標高
    }

    //取得した標高を返す
    return elevation
}

つまずきポイント

どうやってもgetElevationで取得した標高をControllerを通じてViewにわたすことができない。
非同期通信についてたくさん調べてやってみたが全然うまく行かない。通信処理からelevationに標高を代入する前にgetElevationメソッドが終了してしまい、取得した標高をUIに表示することができませんでした。そんなときに助けてくれたのがクロージャでした。

ここで役に立つのかクロージャ!

以前クロージャの記事を書いたが、いまいち使い所がわからなかった。
Swiftクロージャ これいつどうやって使うの?
ところが今回なんとなくわかった、気がする。

まず今回うまく行かなかった原因は、取得した標高を変数に代入する前にメソッドが終了して、意図した戻り値を返すことができていなかったからです。
IMG_E903CC116E29-1.jpeg

これを解決するためにreturnを使うのをやめ、通信処理が完了したらクロージャを実行して値を返すようにします。

クロージャの出番

今までのコードを以下のように書き換えます

Controller
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    model.getElevation(locations, completionHandler: { (elevationString) in
        DispatchQueue.main.async {
            self.customView.elevation = elevationString ?? "標高を取得できませんでした"
        }
    })
}
Model
var elevationString = "0.0m"
func getElevation(_ Locations: [CLLocation], completionHandler: @escaping (String?) -> Void) {
    //通信処理の準備

    APIを使った通信処理 { (data, response, error) in
        //デコードしたりなんやかんやで標高を取得
        elevationString = 取得した標高
        completionHandler(elevationString)

解説

最初にこの書き方を見たときは少し怯んだのですが、自分なりにコードの解説をしてみます。

IMG_89A8AD4E3EDE-1.jpeg

①クロージャの設置

まずクロージャを使ってcompletionHandlerというものを定義しています。このcompletionHandlerはString?型の変数を持っています。

②クロージャに値を代入

国土地理院から標高を取得するという通信処理が完了したら、クロージャを呼び出してもらってその標高を代入します。

③代入された値を利用する

代入された値を利用して、Viewに渡します。

実は③の部分は最初から使っていた以下のコードと同じ操作をしています。

Model
APIを使った通信処理 { (data, response, error) in
    //dataを解析したり
    //errorが返ってきたら出力したり
}

今回だと通信処理が完了するまではクロージャは呼び出されず、何も値を持っていません。通信処理が完了したらクロージャが呼び出され値を代入し、その値をViewに渡すというふうになっています。

まとめ

まだまだ理解しきれていないところはたくさんあります。

  1. @escapingについて 
  2. 循環参照について

クロージャの使い所を実感できただけでも成長したなと思いました。修正やもっとこんなふうに書いたほうがいいよなどありましたらお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS 14:クリップボードにアクセスする前にその内容の種別を検知

iOS 14では、アプリがユーザーのクリップボードのコンテンツにアクセスするたびに、画面上部に警告が表示されます。

アプリがクリップボードのコンテンツにアクセスするのは、その中のコンテンツに気になる点があるかどうか知りたい場合があるためです。

この UIPasteboard.generaldetectPatterns 機能を利用すると、クリップボードのコンテンツが次のカテゴリのいずれかに該当するかどうか検出することができます。

image

  • 数字 .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の記事のリストをカテゴリー別にご覧いただけます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】AlamofireでURLRequestValidationFailureReason.bodyDataInGETRequest(14 bytes)が出た

AlamofireでAPI周りを実装した時に以下のエラーが出ました。

Alamofire.AFError.urlRequestValidationFailed(reason: Alamofire.AFError.URLRequestValidationFailureReason.bodyDataInGETRequest(14 bytes))

どうやらエンコーディングの方法が違いエラーをはいているようです。
この時に指定していたエンコーディングはJSONEncoding.defaultでした。
これをURLEncoding.defaultに変更することでリクエストは正常に処理されました。

結果的に問題は解決出来たのですが、理由は分からず。。
ちょっと前までJSONEncoding.defaultで正常に動作していた気がするのですが、何か知見のある方コメント頂けると嬉しいです?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む