- 投稿日:2021-04-03T19:41:56+09:00
【Swift】RxSwiftまとめ
- 投稿日:2021-04-03T19:36:19+09:00
【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楽しい!
- 投稿日:2021-04-03T17:22:05+09:00
iOS/macCatalystアプリで、指定されたフォルダの中にファイルを書き出す
概要
- iOS 13以降では、
UIDocumentPickerViewControllerでdocumentTypesにkUTTypeFolderを指定すると、フォルダを選択させることが可能になっている。- 選択されたフォルダに対して
startAccessSecurityScopedResourceを呼び出すと、そのフォルダの下にファイルを書き込むことができる。Permissionの設定
App Sandboxで「User Selected File」を「Read/Write」に設定する。
UIDocumentPickerViewControllerで書き込み先フォルダを選ぶ
iOS 13以降では
UIDocumentPickerViewControllerでdocumentTypesにkUTTypeFolderを指定することで、フォルダの選択が可能となっている。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”."参考
- 投稿日:2021-04-03T13:44:42+09:00
【Swift】RxSwift勉強してみたPart3
はじめに
前回に引き続き、RxSwiftを勉強したので、その学習アウトプットです。
RxSwiftでHello World的なものを書いてみます。GitHub
以下のHelloRxSwiftフォルダに今回のプロジェクトはあります。
実装
流れは以下の通りです。
1.HelloWorldSubjectというSubjectを定義
2.Subjectを購読
3.値が流れてきたら
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このような書き方は
ViewControllerとViewModelや遷移元と遷移先のViewControllerのデータの受け渡しで使われます。
次はViewControllerとViewModelに分けて書いてみます。HelloRxViewControllerimport 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() } }HelloRxViewModelclass 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まだまだわからないことだらけ、、、
次回
- 投稿日:2021-04-03T09:17:01+09:00
Xcode de AtCoder
0. はじめに
2年ほど前からAtCoderに参加しています。はじめた頃はVSCode上で
Kotlinを使用していました。しかし、もともとXcode+Swiftでアプリを開発していたことと、AtCoderのジャッジシステムが新しくなったこともあって、昨年4月からSwiftでAtCoderに参加しています。
AtCoderのコンテスト中、テストケースの実行やコードの提出は、AtCoderのコンテストページから/へのコピペで行っていましたが、『これら、自動化できるよね』ということで、今回の記事に至りました。AtCoderに関する自動化については、下記2つの記事が大変参考になります。
Swift言語においても、VSCodeやコマンドラインでコンパル&実行するなら、上記の記事で十分ですが、今回はXcodeのコンソールアプリのプロジェクトを使い回すことを前提にしています。つまり、コンテストや問題ごとにソースファイルを分けずに、常に一つのXcodeプロジェクトで実施しようというものです。これがベストプラクティスとは言いませんが、自分はこの環境でAtCoderに参加したいということです。
1. 今回のゴール
Xcode+SwiftのコンソールアプリにてAtCoderの解答コードを作成し、サンプルテストケースの自動実行とAtCoderへのソースコードの提出を自動化すること。
2. 準備
前出の自動化記事を参考に、
online-judge-toolsとatcoder-cliをインストールしておきます。3. 環境
a) Xcodeのコンソールアプリのプロジェクト
Xcodeで 新規プロジェクト(New → Project) → macOS → Command Line Tool と進み、「プロジェクト名」やプロジェクトの「格納先ディレクトリ」を指定して作成したプロジェクトは、標準入力/標準出力から入出力するコンソールアプリとなります。変更しない限りプロジェクト名=プログラム名です。
b) ソースファイルの場所の確認
上記で指定したプロジェクトフォルダ配下に
main.swiftとして作成されます。このファイルにAtCoderの問題を解くコードを書きます(いわゆるコーディング)。
AtCoderへ提出するコードがこれになるのでフルパスをメモしておきます。c) ビルドして生成されるバイナリファイルの場所の確認
Xcodeでビルドすると、実行ファイルが生成されます。このファイルの場所は、Xcodeのプロジェクトナビゲーターの
Productsの下にあるプロジェクト名と同名の実行ファイルをクリックすると、フルパスを知ることができます。
またShow in Finderを選べば、その場所のファインダーが開きます。
ローカル環境でテストするバイナリファイル(実行ファイル)がこれになるのでフルパスをメモしておきます。
XcodeでRun(実行)すると、このプログラムが起動され、Xcode上のコンソールで入力や出力されます。もちろん、ターミナルを開き、この実行ファイルを直接起動することもできます。4. 自動化
AtCoderの2つの自動化に対応します。
- テストの自動化
- ソースコードの提出
4.1 テストの自動化
AtCoderのコンテストでは各問題ごとに例題として数件のテストケースが提供されます。『この入力を与えると、こう出力されたら正解』といった例です。この例題の入力と出力をもとに、作成したプログラムの動作確認を自動で行うことが目的です。
これには前出のonline-judge-tools(oj)を使用します。ojはターミナルから投入するコマンドです。a) AtCoderへログイン
下記のコマンドを投入します。AtCoderへのログインは、コンテストごとに最初に一回実行しておきます。
$ acc login ? username: ユーザID ? password: パスワード $ oj login -u ユーザID -p パスワード https://atcoder.jp/b) テストケースのダウンロード
サブフォルダ作成とテストケースのダウンロードをまとめて行ってくれる
acc newを使用します。コマンドの引数にコンテストIDを指定します。
この例では、コンテストID:ABC190$ acc new abc190すると、カレントディレクトリに、以下のように
コンテストIDフォルダが作成され、その配下に問題タスクごとのテストケースをダウンロードして入力と出力ファイルが作成されます。. └── ABC190 ├── a │ └── tests │ ├── sample-1.in │ ├── sample-1.out │ ├── sample-2.in │ ├── sample-2.out │ ├── sample-3.in │ └── sample-3.out ├── b │ └── tests │ ├── sample-1.in │ ├── sample-1.out │ ├── sample-2.in │ ├── sample-2.out │ ├── sample-3.in │ └── sample-3.out ├── c │ └── tests │ ├── sample-1.in │ ├── sample-1.out │ ├── sample-2.in │ ├── sample-2.out │ ├── sample-3.in │ └── sample-3.out ├── contest.acc.json ├── d │ └── tests │ ├── sample-1.in │ ├── sample-1.out │ ├── sample-2.in │ ├── sample-2.out │ ├── sample-3.in │ └── sample-3.out ├── e │ └── tests │ ├── sample-1.in │ ├── sample-1.out │ ├── sample-2.in │ ├── sample-2.out │ ├── sample-3.in │ └── sample-3.out └── f └── tests ├── sample-1.in ├── sample-1.out ├── sample-2.in └── sample-2.out
acc contest|tasksコマンドでAtCoderのURLが取得可能です。$ acc contest abc190 AtCoder Beginner Contest 190 https://atcoder.jp/contests/abc190 $ acc tasks abc190 A Very Very Primitive Game https://atcoder.jp/contests/abc190/tasks/abc190_a B Magic 3 https://atcoder.jp/contests/abc190/tasks/abc190_b C Bowls and Dishes https://atcoder.jp/contests/abc190/tasks/abc190_c D Staircase Sequences https://atcoder.jp/contests/abc190/tasks/abc190_d E Magical Ornament https://atcoder.jp/contests/abc190/tasks/abc190_e F Shift and Inversions https://atcoder.jp/contests/abc190/tasks/abc190_fc) テスト
これまでは準備で、やっとテストです。
下記コマンドで実行します。各問題のサブディレクトリに移動して実行します。例えば、A問題はディレクトリaを指定します。$ cd コンテストID $ cd a $ oj test -d tests -c "実行ファイルパス"結果は、下記の通り。それぞれのテストケースが自動でテストされて出力と照合してACとなっています。
実行ファイルパスは固定のため、環境変数SWIFTEXEに設定しています。$ cd ABC190 $ cd a $ oj test -d tests -c "$SWIFTEXE" [INFO] online-judge-tools 11.1.1 (+ online-judge-api-client 10.8.0) [INFO] 3 cases found time: illegal option -- f usage: time [-lp] <command> [WARNING] GNU time is not available: time [INFO] sample-1 [INFO] time: 0.035278 sec [SUCCESS] AC [INFO] sample-2 [INFO] time: 0.009566 sec [SUCCESS] AC [INFO] sample-3 [INFO] time: 0.009297 sec [SUCCESS] AC [INFO] slowest: 0.035278 sec (for sample-1) [SUCCESS] test success: 3 cases4.2 提出の自動化
下記コマンドでソースコードを提出します。コマンドの引数で前述の問題ごとのURLを指定します。
この例では、
コンテストID:ABC190
問題ID:ABC190-A (A問題)
コマンドの引数の意味は、--no-guess:推測しない、-y:応答確認しない、--no-open:提出後にAtCoderのサイトを開かない、 -l:言語を指定、です。$ oj submit --no-guess -y --no-open -l Swift https://atcoder.jp/contests/abc190/tasks/abc190_a "ソースファイルパス"結果は以下の通り。
ソースファイル名は固定のため、環境変数SWIFTSRCに設定しています。$ oj submit --no-guess -y --no-open -l Swift https://atcoder.jp/contests/abc190/tasks/abc190_a "$SWIFTSRC" [INFO] online-judge-tools 11.1.1 (+ online-judge-api-client 10.8.0) [WARNING] cannot guess URL since the given file is not in the current directory : : : : : : [INFO] chosen language: 4055 (Swift (5.2.1)) [WARNING] the problem "https://atcoder.jp/contests/abc190/tasks/abc190_a" is specified to submit, but no samples were downloaded in this directory. this may be mis-operation [INFO] sleep(3.00) : : : : : : [SUCCESS] result: https://atcoder.jp/contests/abc190/submissions/19897304 [INFO] save cookie to: /Users/USERNAME/Library/Application Support/online-judge-tools/cookie.jarソースコードの提出はできましたが、想定外のワーニングが出ています。カレントディレクトリにソースファイルが置かれていないことが原因のようです。
とは言え、今回の目的は問題ごとにソースファイルを分けないので、元のソースファイルをカレントディレクトリにコピーしてこのファイルを指定するようにしました。するとワーニングがなくなります。$ cp $SWIFTSRC ./ $ oj submit --no-guess -y --no-open -l Swift https://atcoder.jp/contests/abc190/tasks/abc190_a main.swift [INFO] online-judge-tools 11.1.1 (+ online-judge-api-client 10.8.0) [INFO] read history from: /Users/USERNAME/Library/Caches/online-judge-tools/download-history.jsonl [INFO] found urls in history: https://atcoder.jp/contests/abc190/tasks/abc190_a [INFO] problem recognized: AtCoderProblem.from_url('https://atcoder.jp/contests/abc190/tasks/abc190_a'): https://atcoder.jp/contests/abc190/tasks/abc190_a [INFO] code (13321 byte): : : : : : : [SUCCESS] result: https://atcoder.jp/contests/abc190/submissions/19897431 [INFO] save cookie to: /Users/USERNAME/Library/Application Support/online-judge-tools/cookie.jar例えば、問題Aから問題Bへの切り替えは、サブディレクトの移動だけですみます。
$ cd ../b5. コマンドレス化(CUI化)
上手く実現できる方式がなかなか見つからないのでGUI化を見送って、上記までの種々のコマンドをシェルスクリプトに閉じ込めて1文字のキャラクタをタイプするだけで実行可能にしました。
$ . ~/atc コンテストIDスクリプトの中身は、①login実行、②環境変数等を設定、③
acc new コンテストIDの実行、④下記コマンドのfunction定義、です。
コマンド 意味 内容 a A問題を選択 サブディレクトをA問題に切り替え b B問題を選択 サブディレクトをB問題に切り替え c C問題を選択 サブディレクトをC問題に切り替え d D問題を選択 サブディレクトをD問題に切り替え e E問題を選択 サブディレクトをE問題に切り替え f F問題を選択 サブディレクトをF問題に切り替え t テスト 選択中の問題のテスト s 提出 選択中の問題のソースコードを提出 コンテスト終了後に、logoutは手動で実行します。
6. おわりに
前出記事の真似に過ぎませんが、実際のコンテストに数回参加し実践しましたが、超絶便利になりました。自画自賛ですが、AtCoderされている方にはぜひ自動化をお勧めします。
ジャッジ結果がモーダルで表示される、stanicさんが開発したプラグインも大変便利です。こちらを参照してください。⇒ AtCoderResultNotifier - AtCoderの提出結果を通知するUserScriptを作成しました
これを使うために、SafariからGoogle Chromeに変えました。こちらのuserscriptsをSafariにインストールすれば、上記のプラグインがSafariでちゃんと動きました。AtCoderResultNotifierの「スクリプトをインストール」ではインストールできないのでちょっと手こずりました。New Remoteを選び、Enter remote url:に
https://greasyfork.org/scripts/371225-atcoderresultnotifier/code/AtCoderResultNotifier.user.jsを入れてOK。ファイルが読み込まれたら、右下のSaveをクリック。みなさんの参考になれば幸いです。以上
参考情報
- 投稿日:2021-04-03T06:19:54+09:00
【Swift】CollectionViewのカスタムセルをタップすることでUISwitchを切り替える
どういうことか
こういうことがやりたい。
どうするのか
- xibを使ってUIを作るが、この際にUIButtonでセル全体を覆ってしまうのがコツ
- どちらのセルをタップしたのかの判定も入れる
- Delegateを使う
カスタムセル側
ToggleCollectionViewCell.swiftprotocol ToggleCollectionViewCellDelegate { func toggleSwitchAction(toggleSwitchCheck: Bool, type: String) } class ToggleCollectionViewCell: UICollectionViewCell, Reusable { @IBOutlet weak var toggleSwitch: UISwitch! var toggleSwitchCheck = false // どっちのセルをタップしたのか判定するための変数 var contentType = "" var delegate: ToggleCollectionViewCellDelegate? override func awakeFromNib() { super.awakeFromNib() } @IBAction func titleButtonAction(_ sender: Any) { // スイッチの切替処理 if toggleSwitchCheck { toggleSwitch.setOn(false, animated:true) toggleSwitchCheck = false } else { toggleSwitch.setOn(true, animated:true) toggleSwitchCheck = true } // スイッチの判定とセルの判定を渡してあげる delegate?.toggleSwitchAction(toggleSwitchCheck: toggleSwitchCheck, type: contentType) } }Controller側
Delegateメソッドはextensionに書きます(自由ですが)。
セルをタップすることでController内のメンバ変数が切り替わるので、それによってなんらかの処理をしてあげるといい感じです。ViewController.swift// メンバ変数 var areYaru = false var koreYaru = false // 中略 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = CollectionViewUtility.createCell(collectionView, identifier: ToggleCollectionViewCell.reusableIdentifier, indexPath) as! ToggleCollectionViewCell switch (indexPath.section) { case 0: cell.titleButtonLabel.setTitle("あれをやる", for: .normal) cell.delegate = self cell.contentType = "are" return cell case 1: cell.titleButtonLabel.setTitle("これをやる", for: .normal) cell.delegate = self cell.contentType = "kore" return cell default: return UICollectionViewCell() } } // 中略 extension ViewController: ToggleCollectionViewCellDelegate { // Delegateメソッド func toggleSwitchAction(toggleSwitchCheck: Bool, type: String) { if type == "userId" { areYaru = toggleSwitchCheck } else if type == "biometrics" { koreYaru = toggleSwitchCheck } else { print("error") } } }おわり(´・ω・`)
- 投稿日:2021-04-03T02:10:45+09:00
【Swift】RxSwift勉強してみたPart2
はじめに
GitHub
今回のリポジトリは以下のGitHubのExampleRxSwiftです。
実装
以下のように、テキストフィールドの文字数に応じて残りの入力可能文字数がわかるというものをつくってみましょう。
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) } }おわりに
まだまだ続きます!
次回


