LoginSignup
5
2

More than 1 year has passed since last update.

【Combine】dropFirst(_:) オペレータを理解する

Last updated at Posted at 2022-01-16

Combineを真面目に勉強しようと思い立った今年です。
というわけで、早速1つ目の記事を書こうと思います。

Combine自体への興味はもちろん大きいのですが、
これを学習することでリアクティブプログラミングへの理解を深めたいという目的もあります。

早速ですが、本題に入るにあたり少し前置きをさせてください。:pray:

学習する上で、「Combineをはじめよう」という宇佐美さんが書かれた本をよく読ませていただいています。
その本の中にこんな文章があります。

プログラマであれば、言葉であれこれ説明する前に、実際にコードを書いて動かしてみるのが理解が早いだろうと考えています。

私はその考えに則ったこの本の構成が非常に好みで、頭にすっと入ってきました。
なので、この記事でもそれを真似させていただきながら書いていこうと思います。

Combineについて書く初めての記事なので、多少前置きが多いですが、
次回からは省略します。

環境

【Xcode】13.1
【Swift】5.5
【macOS】Big Sur バージョン 11.4

公式ドキュメントから理解する

個人的な話で恐縮ですが今年の目標は、iOSの開発力をあげることです。
それにあたり、公式ドキュメントをきちんと読むことを忘れずにいこうと思っています。

また、念の為公式ドキュメントだけではなく、コード内に書かれているドキュメントコメントもちゃんと読もうと思います。
ドキュメントを読む習慣をつけることが目的です。

以上前置きでした。


dropFirst()オペレータに関してのドキュメントにあったコードはこちらです。

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
cancellable = numbers.publisher
    .dropFirst(5)  // ここで使用されている
    .sink { print("\($0)", terminator: " ") }

これを実行すると、以下が出力されます。

6 7 8 9 10

このコードは一体何をしているのか、ドキュメントの記述から理解していこうと思います。

ドキュメントによりますと、
「連続する要素を再パブリッシュする前に、特定の数の要素を除外する」

Omits the specified number of elements before republishing subsequent elements.

とのこと。

定義はこのようになっています。

func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self>

サンプルコードには、dropFirst(1)と明示的に1を指定しているものも多いですが、
デフォルト値が既に1なので、dropFirst()でも良いですね。

公式ドキュメントのサンプルコードでは、
dropFirst(5)のオペレータにより、初めの5要素(1,2,3,4,5)がスキップされてprintされているのがわかります。

実際の使用例

Apple公式ドキュメントのサンプルコードは、挙動を理解する手助けにはなるものの
実際どのような場面で使用するのか、個人的にはよくわかりませんでした。。

そこで、別のサンプルコードを引っ張ってきました。

まずViewのコードです。

import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()

    var body: some View {
        VStack(spacing: 20) {
            Text("Create a User ID")

            TextField("user id", text: $viewModel.userId)
                .padding()
                .border(statusColor)
                .padding()
        }
        .font(.title)
    }
}

private extension ContentView {
    // textfieldの入力値によって、textfieldのボーダーカラーを変更する
    private var statusColor: Color {
        switch viewModel.isUserIdValid {
        case .ok: return .green
        case .invalid: return .red
        case .notEvaluated: return .secondary
        }
    }
}

TextFieldが1つ存在している画面になります。
TextFieldのボーダーカラーは、入力値によって変化します。

次にViewModelクラスのコードです。

import Foundation

enum Validation {
    case ok
    case invalid
    case notEvaluated
}

class ContentViewModel: ObservableObject {
    @Published var userId = ""
    @Published var isUserIdValid = Validation.notEvaluated

    init() {
        // ViewModelの初期化時に、userIdに空文字が割り当てられるため、このパイプラインは実行される
        $userId
            .dropFirst()  // ここで使用されている
            .map { userId -> Validation in
                userId.count > 8 ? .ok : .invalid
            }
            .assign(to: &$isUserIdValid)
    }
}

TextFieldの入力値を変化を監視しています。ViewModelが初期化されるとき、userIdプロパティに空文字が割り当てられるため、$userId以下のパイプラインが実行されます。

ここでのコードより、
入力値が正当、つまり8文字以上の場合はボーダーカラーはグリーン(.ok
不正な場合はレッド(.invalid
まだ評価されていない場合は、グレー(.notEvaluated
になることがわかります。

それでは仮にdropFirst()の行をコメントアウトしてアプリを実行した場合、
textFieldのボーダーカラーは何色になるでしょうか。

正解は・・・

「レッド」です。

ユーザーが何も入力していないのに、レッドになってしまいます。
ですがまだtextFieldへの入力値がないのですから、未評価扱いつまりグレーにならなければいけないはずです。

こういった挙動を防ぐため、dropFirst()は使用されます。
前述したように、$userId以下のパイプラインが初めて実行されるのはいつかというと、
Viewが読み込まれ、ViewModelが初期化されたときです。

つまりアプリを初めて起動した段階で、パイプラインは実行されます。

そのため、dropFirst()が存在しない場合、
mapオペレータ内の8文字以下以上のロジック判断が行われ、
当然0文字なので不正値(.invalid)と判断されて、
最終的にisUserValidプロパティの値は.invalidとなるので、
TextFieldのボーダーカラーはレッドになってしまいます。

dropFirst()を使用すれば、
1回目のパブリッシュがスキップされるため、8文字以下以内の判断は行われません。
なのでisUserValidは初期化された時の.notEvaluatedのままになり、
初回起動時のTextFieldのボーダーカラーもめでたくグレーのままになります。

上記のコード全体は、以下に上がっていますので、ご参考ください。

RxSwiftでは

こちらも少し前置きさせてください。:pray:

まず始めに、私はRxSwiftは触ったことはありません。。そして今回の記事をかくにあたっても、RxSwiftは触っていません。。

ですが以前読んだ比較記事や動画によると、実装の詳細は違えど、APIはかなり似ているという意見が多く見られました。
またCombineがiOS13から使用可能と言うこともあり、RxSwiftの需要もまだ大きいようです。

こちらのスクラップにちょこちょこメモ書いてます。↓

そこで今後もし自分がRxSwiftに携わる経験が出てきたら、
ああCombineで言うとこれねと言うのがわかるようにしておこうと思い、ここに書いておきます。

先輩エンジニアは私とは逆で、RxSwiftでいうとこれねになると思いますが、
私はCombineからリアクティブプログラミングに入門しているので、今後使用される可能性が高くなることを願ってCombineから勉強しております。

RxSwiftでいうとこれね、と言うのは、既に「RxSwift to Combine Cheatsheet」というのがあるので、そのリポジトリで調べた対応するオペレータを載せておき、
RxSwiftの公式ドキュメントを確認するに留めようと思います。

こちらも前置きが長くなりました。本題に戻ります。


CombineのdropFist()オペレータに対して、
RxSwiftのオペレータは、skip()になるようです。

ドキュメントにはこう書かれていました。

「監視対象のシーケンスで指定された要素数をスキップし、残りの要素を返す」

Bypasses a specified number of elements in an observable sequence and then returns the remaining elements.

定義はこう。
こちらは、Combineと違ってデフォルト値は設定されていないので、必ずcountにInt型の数値を指定する必要があります。

public func skip(_ count: Int)
    -> Observable<Element>

Combine側のサンプルコードで、dropFirst(1)と明示的に指定しているコードが多いのは
RxSwiftの名残りなのかなーと思いました。 :thinking:

おわりに

今回調べたのはdropFirstオペレータですが、dropがつくオペレータは他にも以下があるようです。

drop(untilOutputFrom:)
drop(while:)
tryDrop(while:)

そしてcheat sheetをみると、それぞれに対応するRxSwiftのオペレータもあるようですね。

まだこの3つは使ったことがありませんが、今後使うことになったらもっと詳しく調査したいと思います。

前述の通りCombine初心者ですので、何か間違い等ありましたらコメントお待ちしています。 :blush:

参考

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2