Closed18

iOSDCのCombineOperatorガイド(宇佐見さん)

Yamamoto ShoheiYamamoto Shohei

大体RxJavaなどと同じように考えればよさそう。
Subjectなどの概念。

Combineでは

  • Publisher > イベントを送信するオブジェクト
  • Publisherからイベントを受信することをsubscribeと呼ぶ

(Rxと一緒)

Yamamoto ShoheiYamamoto Shohei

List1はこんな感じにしてみた

  • Receiverが直接subjectを参照しているのが気になったので、initの引数にしてみたSubjectで行きたかったが、エラーが出たので、PassthroughSubject<String, Never>にした。
  • subscriptionをキャンセルするとその後イベントがこないことも確認してみた。

let subject = PassthroughSubject<String, Never>()

final class Receiver {
    private var subscriptions = Set<AnyCancellable>()
    
    init(subject: PassthroughSubject<String, Never>) {
        subject.sink { value in
            print("Received value:", value)
            print("subscriptions:", self.subscriptions.count)
            if value == "い" {
                self.subscriptions.first!.cancel()
            }
        }
        .store(in: &subscriptions)
    }
}

let reciever = Receiver(subject: subject)

subject.send("あ")
subject.send("い")
subject.send("う")

Yamamoto ShoheiYamamoto Shohei

リスト2について

  • assignはおしゃれ
    • \.valueのようなのをSwiftUIでもよくみるがよくわかっていなかった。(KeyPath)
    • ViewModelの値をViewに反映させるときなどに使うと良い。
Yamamoto ShoheiYamamoto Shohei

keypath試した
使い所はまだわかっていない

import Foundation

struct Hamster {
    let name: String
    var age: Int
    
    func run() -> String {
        "Fun"
    }
}

var hamster = Hamster(name: "Tofu", age: 0)
print(hamster.run())

let anyKeyPath: AnyKeyPath = \Hamster.age

print(hamster[keyPath: anyKeyPath]!) // 0
print(type(of: anyKeyPath).rootType) // Hamster
print(type(of: anyKeyPath).valueType) // Int


// AnyKeyPathはReadOnly
//hamster[keyPath: anyKeyPath] = 1 // コンパイルエラー 'hamster' is immutable


let partialKeyPath: PartialKeyPath<Hamster> = \.name

print(hamster[keyPath: partialKeyPath])

let writableKeyPath: WritableKeyPath<Hamster, Int> = \.age
print(hamster.age)
hamster[keyPath: writableKeyPath] = 1
print(hamster.age) // 値が変化
Yamamoto ShoheiYamamoto Shohei

List3

  • mapで途中に処理を追加できる
  • mapのようにあるPublisherを別のPublisherに変換するメソッドをOperatorという
Yamamoto ShoheiYamamoto Shohei

List4

  • ViewModelにPublisherを持たせ、それをVCでsubscribeすることで、責務の分離できる
  • VCはassignを使ってlabelに値を入れるだけ。
  • eraseToAnyPublisherによって型消去ができる
    • これは、mapなどを通していくことで、型がどんどん入子構造になり、利用時の型指定に困る(これはSwiftUIでもあった気がする)。これをAnyPyblisherという方に変えてくれる
    • publisherをプロパティに持たせるときに定番で利用する
Yamamoto ShoheiYamamoto Shohei
  • mapは普通の変換
  • replaceNilでnilの時の値を指定して変換できる
  • compactMapはnilを送信できないようにできる
Yamamoto ShoheiYamamoto Shohei

List11

  • tryMapでthrowするとsinkのreceiveCompletionで出力できる

追加

  • subject.send(completion: .finished)で正常終了できる
Yamamoto ShoheiYamamoto Shohei

List12

  • retryはエラー終了に対して、再接続を指定回数分試みる

例えばretry(2)で以下のようにすると、

subject.send("1")
subject.send("2")
subject.send("3")
subject.send("x")
subject.send("4")
subject.send("5")
subject.send("x")
subject.send("6")
subject.send("7")
subject.send("x")
subject.send("8")
subject.send("x")
subject.send("9")

結果は以下になる。つまりretry回数はカウントアップされている。正常値が入るとリセットされるのではなく、2回までretryするという意味

Value:  1
Value:  2
Value:  3
Value:  4
Value:  5
Value:  6
Value:  7
Completion: 
 failure(__lldb_expr_124.MyError.failToConvert)
Yamamoto ShoheiYamamoto Shohei

フィルタリングのoperator

import Combine

let subject = PassthroughSubject<Int, Never> ()

subject.filter{$0 % 2 == 0}.sink(receiveValue: {value in print("filter % 2 == 0: ", value)})
//subject.removeDuplicates().sink(receiveValue: {value in print("removeDuplicates: ", value)})
//subject.drop(while: {$0 % 2 == 1}).sink(receiveValue: {value in print("drop while %2 == 1", value)})
//subject.dropFirst(2).sink(receiveValue: {value in print("dropFirst 2: ",value )})
//subject.prefix(2).sink{value in print("prefix 2: ", value)}

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(3)
subject.send(3)
subject.send(4)
subject.send(5)
subject.send(6)

  • filter 条件に一致するものをPublish
Yamamoto ShoheiYamamoto Shohei
  • removeDuplicates 1度出たものは2度と出さない

以下のように3を離して出しても1度しか出ない。

subject.send(1)
subject.send(2)
subject.send(3)
subject.send(3)
subject.send(3)
subject.send(4)
subject.send(5)
subject.send(6)
subject.send(3)
subject.send(3)
subject.send(6)

結果

removeDuplicates:  1
removeDuplicates:  2
removeDuplicates:  3
removeDuplicates:  4
removeDuplicates:  5
removeDuplicates:  6
Yamamoto ShoheiYamamoto Shohei
  • drop while
    • drop(while: {$0 % 2 == 1})
    • 条件に当てはまるまで無視。当てはまったら条件に当てはまらなくても出す
Yamamoto ShoheiYamamoto Shohei
  • combineLatest
    • 値が変化したときに2つのstreamの最新の値を表示
import Combine

let subjectX = PassthroughSubject<String, Never>()
let subjectY = PassthroughSubject<String, Never>()

final class SomeObject {
    var value: String = "" {
        didSet {
            print("didSet value: ", value)
        }
    }
}

let object = SomeObject()

subjectX.combineLatest(subjectY)
    .map { x, y in
        x + " + " + y
    }
    .assign(to: \.value, on: object)

subjectX.send("1")
subjectX.send("2")
subjectY.send("a")
subjectY.send("b")
subjectX.send("3")
subjectY.send("c")
didSet value:  2 + a
didSet value:  2 + b
didSet value:  3 + b
didSet value:  3 + c
Yamamoto ShoheiYamamoto Shohei
  • zip
    • 2つ揃ったら順番に出す

import Combine

let subjectX = PassthroughSubject<String, Never>()
let subjectY = PassthroughSubject<String, Never>()

final class SomeObject {
    var value: String = "" {
        didSet {
            print("didSet value: ", value)
        }
    }
}

let object = SomeObject()


subjectX.zip(subjectY)
    .map { x, y in
        x + " + " + y
    }
    .assign(to: \.value, on: object)

subjectX.send("1")
subjectX.send("2")
subjectY.send("a")
subjectY.send("b")
subjectX.send("3")
subjectY.send("c")
subjectY.send("d")

結果

didSet value:  1 + a
didSet value:  2 + b
didSet value:  3 + c
Yamamoto ShoheiYamamoto Shohei

import Combine

let subjectX = PassthroughSubject<String, Never>()
let subjectY = PassthroughSubject<String, Never>()

final class SomeObject {
    var value: String = "" {
        didSet {
            print("didSet value: ", value)
        }
    }
}

let object = SomeObject()


subjectX.merge(with: subjectY)
    .assign(to: \.value, on: object)

subjectX.send("1")
subjectX.send("2")
subjectY.send("a")
subjectY.send("b")
subjectX.send("3")
subjectY.send("c")
subjectY.send("d")

結果

didSet value:  1
didSet value:  2
didSet value:  a
didSet value:  b
didSet value:  3
didSet value:  c
didSet value:  d

このスクラップは2021/09/12にクローズされました