20210403のSwiftに関する記事は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で続きを読む

【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で続きを読む

Xcode de AtCoder

0. はじめに

2年ほど前からAtCoderに参加しています。はじめた頃はVSCode上でKotlinを使用していました。しかし、もともとXcode+Swiftでアプリを開発していたことと、AtCoderのジャッジシステムが新しくなったこともあって、昨年4月からSwiftでAtCoderに参加しています。
AtCoderのコンテスト中、テストケースの実行やコードの提出は、AtCoderのコンテストページから/へのコピペで行っていましたが、『これら、自動化できるよね』ということで、今回の記事に至りました。

AtCoderに関する自動化については、下記2つの記事が大変参考になります。
  1. AtCoderの提出自動化をする(Python)

  2. atcoder初心者こそ環境構築しよう!(atcoder-cli,online-judge-toolsのインストール、使い方)

Swift言語においても、VSCodeやコマンドラインでコンパル&実行するなら、上記の記事で十分ですが、今回はXcodeのコンソールアプリのプロジェクトを使い回すことを前提にしています。つまり、コンテストや問題ごとにソースファイルを分けずに、常に一つのXcodeプロジェクトで実施しようというものです。これがベストプラクティスとは言いませんが、自分はこの環境でAtCoderに参加したいということです。

1. 今回のゴール

Xcode+SwiftのコンソールアプリにてAtCoderの解答コードを作成し、サンプルテストケースの自動実行とAtCoderへのソースコードの提出を自動化すること。

2. 準備

前出の自動化記事を参考に、online-judge-toolsatcoder-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つの自動化に対応します。

  1. テストの自動化
  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_f

c) テスト

これまでは準備で、やっとテストです。
下記コマンドで実行します。各問題のサブディレクトリに移動して実行します。例えば、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 cases 

4.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 ../b

5. コマンドレス化(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をクリック。

みなさんの参考になれば幸いです。以上

参考情報

online-judge-tools/oj

atcoder-cli チュートリアル

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

【Swift】CollectionViewのカスタムセルをタップすることでUISwitchを切り替える

どういうことか

こういうことがやりたい。

d760189770a3a5b3dedc0b5689ae8206.gif

どうするのか

  • xibを使ってUIを作るが、この際にUIButtonでセル全体を覆ってしまうのがコツ
  • どちらのセルをタップしたのかの判定も入れる
  • Delegateを使う

カスタムセル側

ToggleCollectionViewCell.swift
protocol 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")
        }
    }
}

おわり(´・ω・`)

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む