20210403のiOSに関する記事は7件です。

【Swift】RxSwiftまとめ

はじめに

自分のRxSwiftに関する記事をまとめておきます。

まとめ

おわりに

RxSwift理解できる日が来るのだろうか、、、?

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

【Swift】RxSwift勉強してみたPart4

はじめに

前回
今回は、今まで出てきた用語をもう少し深ぼって学習していきたいと思います。

解説

Observable(観測可能)

イベントを検知するためのクラス
ストリームと言われたりする
Observableが通知するイベントは以下のようなものがある。
・onNext
デフォルトのイベントをながす
イベント内に値を格納でき、何度でも呼び出せる
・onError
エラーイベント
一度だけ呼ばれ、その時点で終了
購読を破棄
・onCompleted
完了イベント
一度だけ呼ばれ、その時点で終了
購読を破棄

ObservableとObserver

Observable: イベント発生元
Observer: イベント処理
です。例えば、この以下のような感じです。

hogeObservable // Observable
    .map { $0 * 10 } // Observable
    .subscribe(onNext: {
        // Observer
    })
    .disposed(by: disposeBag)

disposed

購読を良きタイミングで破棄してメモリリークを回避するための仕組み

SubjectとRelay

イベントの検知に加えてイベント発生もできる便利なクラス
良い使われるもの

流せるイベント バッファ
PublishSubject onNext, onError, onComplete 持たない
BehaviorSubject onNext, onError, onComplete 持つ
PublishRelay onNext 持たない
BehaviorRelay onNext 持つ

バッファ

BehaviorSubject/Relayは、subscribe時に1つ過去のイベントを受け取ることができる。
最初にsubcribeするときは、宣言時に設定した初期値を受け取る。

SubjectとRelay使い分け

・Subject
通信処理やDB処理等のエラーが発生した時にその内容によって処理を分岐させたい
・Relay
UIに値をBindする

bind

Observable/Observerに対してbindメソッドを使うと指定したものにイベントストリームを接続できる
単方向のデータバインディング
subscribeして値をセットしているだけ

final class HogeViewController: UIViewController {

    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var nameLabel: UILabel!
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        //bind利用(subscribeして値をセット)
        nameTextField.rx.text
            .bind(to: nameLabel.rx.text)
            .disposed(by: disposeBag)

        //subscribe利用
        nameTextField.rx.text
            .subscribe(onNext: { [weak self] text in
                self?.nameLabel.text = text
            })
            .disposed(by: disposeBag)

    }
}

Operator

Observableから流れてきた値をそのままsubscribe(またはbind)するのではなく、途中で値を加工してsubscribe(bind)をしたいときに使う。

・変換

概略 Operator 説明
変換 map 通常の高階関数と同じ動き
flatMap 通常の高階関数と同じ動き
reduce 通常の高階関数と同じ動き
scan reduceに似ていて、途中結果もイベント発行できる
debounce 指定時間イベントが発生しなかったら、最後に流されたイベントをながす
絞り込み filter 通常の高階関数と同じ動き
take 指定時間の間だけイベントを通知してonCompletedする
skip 指定時間の間はイベントを無視する
distinct 重複イベントを除外する
組み合わせ zip 複数のObservableを組み合わせる(異なる型でも可能)
merge 複数のObservableを組み合わせる(異なる型では不可能)
combineLatest 複数のObservableの最新値を組み合わせる(異なる型でも可能)
sample 引数にわたしたObservableのイベントが発生されたら、元のObservableの最新イベントを通知
concat 複数のObservableのイベントを順番に組み合わせる(異なる型では不可能)

map

//nameTextFieldのテキスト文字数を数えてnameLabelのテキストに反映
nameTextField.rx.text
    .map { text -> String? in
        guard let text = text else { return nil }
        return "あと\(text.count)文字"
    }
    .bind(to: nameLabel.rx.text)
    .disposed(by: disposeBag)
//ボタンをタップしたときにnameLabelにユーザーの名前を表示する
let user = User(name: "REON")
showUserNameButton.rx.tap
    .map { [weak self] in
        return self?.user.name
    }
    .bind(to: nameLabel.rx.text)
    .disposed(by: disposeBag)

filter

//整数が流れるObservableから偶数のイベントのみに絞り込んでevenObservableに流す
numberSubject
    .filter{ $0 * 2 == 0 }
    .bind(to: evenSubject)
    .disposed(by: disposeBag)

zip

//複数のAPIにリクエストして同時に反映したい場合に使える
Observable.zip(firstApiObservable, secondApiObservable)
    .subscribe(onNext: { (firstApi, secondApi) in
        // ...
    })
    .disposed(by: disposeBag)

HotとColdなObservable

HotなObservable

・subscribeされなくても動作する
・複数の箇所でsubscribeした時に全てのObservableで同じイベントが同時に流れる

ColdなObservable

・subscribeした時に動作する
・単体では意味がない
・複数の箇所でsubscribeしたとき、それぞれのObservableでそれぞれのイベントが流れる
・使い所は非同期通信処理

おわりに

RxSwift楽しい!

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

iOS/macCatalystアプリで、指定されたフォルダの中にファイルを書き出す

概要

  • iOS 13以降では、UIDocumentPickerViewControllerdocumentTypeskUTTypeFolderを指定すると、フォルダを選択させることが可能になっている。
  • 選択されたフォルダに対してstartAccessSecurityScopedResourceを呼び出すと、そのフォルダの下にファイルを書き込むことができる。

Permissionの設定

App Sandboxで「User Selected File」を「Read/Write」に設定する。

UIDocumentPickerViewControllerで書き込み先フォルダを選ぶ

iOS 13以降ではUIDocumentPickerViewControllerdocumentTypeskUTTypeFolderを指定することで、フォルダの選択が可能となっている。

import MobileCoreServices // kUTTypeFolderを参照するために必要

...

let documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String], in: .open)
documentPicker.delegate = self
present(documentPicker, animated: true, completion: nil)

選択されたディレクトリの下にファイルを書き込む

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
    guard let directoryURL = urls.first else {
        return
    }
    debugPrint(directoryURL)

    guard directoryURL.startAccessingSecurityScopedResource() else {
        // Handle the failure here.
        return
    }
    defer { directoryURL.stopAccessingSecurityScopedResource() }

    do {
        try "Hello World".write(to: directoryURL.appendingPathComponent("hello.txt"), atomically: true, encoding: .utf8)
        try "Good Morning".write(to: directoryURL.appendingPathComponent("morning.txt"), atomically: true, encoding: .utf8)
    } catch let error {
        debugPrint(error)
    }
}

startAccessingSecurityScopedResource()を呼び出さないと、ファイルの書き込みができないので注意。

Error Domain=NSCocoaErrorDomain Code=513 "You don’t have permission to save the file “hello.txt” in the folder “com~apple~CloudDocs”."

参考

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

iCloudにログインしているユーザーのユーザーIDを取得する

CloudKitを利用すると、iCloudでログインしているユーザーのIDを取得することができます。

以下のように、CKContainerに対してfetchUserRecordIDを実行すると、ユーザーIDがコールバックで取得できます。

let container = CKContainer(identifier: "iCloud.xxx.xxx.xxx") // 作成したコンテナのID
container.fetchUserRecordID { [weak self] (id, error) in
    DispatchQueue.main.async {
        if let error = error as? CKError {
            switch error.errorCode {
            case CKError.notAuthenticated.rawValue:
               // 認証していない場合の処理
            case CKError.managedAccountRestricted.rawValue:
               // 利用制限をかけている場合の処理
            default:
               // その他のエラー処理
            }
        } else {
            // IDが取得できた時の処理
        }
    }
}

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

【Swift】RxSwift勉強してみたPart3

はじめに

前回に引き続き、RxSwiftを勉強したので、その学習アウトプットです。
RxSwiftでHello World的なものを書いてみます。

GitHub

以下のHelloRxSwiftフォルダに今回のプロジェクトはあります。

実装

流れは以下の通りです。
1.HelloWorldSubjectというSubjectを定義
2.Subjectを購読
3.値が流れてきたらprintで値を出力されるように定義
4.定義したクラスが破棄されたら購読も自動的に破棄させる
5.N回イベントをながす
6.定義したクロージャがN回実行される

viewDidLoadでこのように書いてみます。

import UIKit
import RxSwift

class HelloRxViewController: UIViewController {

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let helloWorldSubject = PublishSubject<String>()
        helloWorldSubject.subscribe(onNext: { message in
            print("onNext: \(message)")
        }, onCompleted: {
            print("onCompleted")
        }, onDisposed: {
            print("onDisposed")
        })
        .disposed(by: disposeBag)

        helloWorldSubject.onNext("HelloWorld!")
        helloWorldSubject.onCompleted()

    }
}
出力結果
//onNext: HelloWorld!
//onCompleted
//onDisposed

このような書き方はViewControllerViewModelや遷移元と遷移先のViewControllerのデータの受け渡しで使われます。
次はViewControllerViewModelに分けて書いてみます。

HelloRxViewController
import UIKit
import RxSwift

class HelloRxViewController: UIViewController {

    private let disposeBag = DisposeBag()
    private let viewModel = HelloRxViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.helloWorldObservable
            .subscribe(onNext: { message in
                print("onNext: \(message)")
            }, onCompleted: {
                print("onCompleted")
            }, onDisposed: {
                print("onDisposed")
            })
            .disposed(by: disposeBag)

        viewModel.updateItem()

    }

}
HelloRxViewModel
class HelloRxViewModel {

    var helloWorldObservable: Observable<String> {
        return helloWorldSubject.asObservable()
    }
    private let helloWorldSubject = PublishSubject<String>()

    func updateItem() {
        helloWorldSubject.onNext("Hello World!")
        helloWorldSubject.onCompleted()
    }

}
出力結果
//onNext: HelloWorld!
//onCompleted
//onDisposed

おわりに

RxSwiftまだまだわからないことだらけ、、、
次回

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

[速報] [iOS] Adjust SDKをバージョンアップしないと審査リジェクトの危険性アリ

表題の通り、Adjust SDKをアップデートしないと、アプリ審査でリジェクトを食らう可能性があるようです。

ご存知の通り、Appleから告知されていた「App Tracking Transparency」が背景にあり、こちらのWebニュースなどが参考になると思います。
https://iphone-mania.jp/news-358057/

Adjust SDKはVersion 4.28.0で対応されている模様です。
該当issueはこちらです。
https://github.com/adjust/ios_sdk/pull/526

2021年4月1日の投稿より引用:

We received reports that some app updates using our SDK v4.27.x and lower have been rejected by Apple. To address this problem, we have released v4.28.0. We recommend that all clients upgrade to SDK v4.28.0 before submitting their next app update.

(Google翻訳)
SDKv4.27.x以下を使用した一部のアプリのアップデートがAppleによって拒否されたという報告を受けました。この問題に対処するために、v4.28.0をリリースしました。次のアプリの更新を送信する前に、すべてのクライアントをSDKv4.28.0にアップグレードすることをお勧めします。

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

【Swift】RxSwift勉強してみたPart2

はじめに

前回の続きです!
合わせてこちらもご覧ください!

GitHub

今回のリポジトリは以下のGitHubのExampleRxSwiftです。

実装

以下のように、テキストフィールドの文字数に応じて残りの入力可能文字数がわかるというものをつくってみましょう。
スクリーンショット 2021-04-03 2.02.49.png

RxSwiftを使わない場合は簡単ですね。

import UIKit

final class ExampleViewController: UIViewController {

    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var addressTextField: UITextField!
    @IBOutlet private weak var addressLabel: UILabel!

    private let maxNameTextFieldSize = 10
    private let maxAddressTextFieldSize = 50
    private let limitText: (Int) -> String = {
        return "あと\($0)文字"
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        nameTextField.addTarget(self,
                                action: #selector(nameTextFieldEditingChanged(sender:)),
                                for: .editingChanged)

        addressTextField.addTarget(self,
                                   action: #selector(addressTextFieldEditingChanged(sender:)),
                                   for: .editingChanged)

    }

    @objc func nameTextFieldEditingChanged(sender: UITextField) {
        guard let changedText = sender.text else { return }
        let limitCount = maxNameTextFieldSize - changedText.count
        nameLabel.text = limitText(limitCount)
    }

    @objc func addressTextFieldEditingChanged(sender: UITextField) {
        guard let changedText = sender.text else { return }
        let limitCount = maxAddressTextFieldSize - changedText.count
        addressLabel.text = limitText(limitCount)
    }

}

RxSwiftを使うとこのようになります。

import UIKit
import RxSwift
import RxCocoa
import RxOptional

final class RxExampleViewController: UIViewController {

    @IBOutlet private weak var nameTextField: UITextField!
    @IBOutlet private weak var nameLabel: UILabel!
    @IBOutlet private weak var addressTextFiled: UITextField!
    @IBOutlet private weak var addressLabel: UILabel!

    private let maxNameTextFieldSize = 10
    private let maxAddressTextFieldSize = 50
    private let limitText: (Int) -> String = {
        return "あと\($0)文字"
    }
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        nameTextField.rx.text
            .map { [weak self] text -> String? in
                guard let text = text else { return nil }
                guard let maxNameTextFieldSize = self?.maxNameTextFieldSize else { return nil }
                let limitCount = maxNameTextFieldSize - text.count
                return self?.limitText(limitCount)
            }
            .filterNil()
            .bind(to: nameLabel.rx.text)
            .disposed(by: disposeBag)

        addressTextFiled.rx.text
            .map { [weak self] text -> String? in
                guard let text = text else { return nil }
                guard let maxAddressTextFieldSize = self?.maxAddressTextFieldSize else { return nil }
                let limitCount = maxAddressTextFieldSize - text.count
                return self?.limitText(limitCount)
            }
            .filterNil()
            .bind(to: addressLabel.rx.text)
            .disposed(by: disposeBag)

    }

}

おわりに

まだまだ続きます!
次回

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