16
5

More than 3 years have passed since last update.

CombineのObservableObjectでinputとoutputを明確にしたい

Last updated at Posted at 2020-04-25

はじめに

SwiftUIでも少しでも複雑な画面になってくるとCombineのObservableObjectの入力と出力を分けたいときがあるわけです。ぱっと見で何が入力で出力なのか整理したいし、他の人にも整理してコードを書いてほしい。

RxSwiftのときはRxSwift研究読本3 ViewModel設計パターン入門のようにRxCocoaがあるので工夫ができるわけだけど、SwiftUI+Combineではどうしていくと整理されるんだろうというわけで、この記事は簡単なサンプルを作って複数のやり方を示してみます。

かんたんな画面例

2つのTextFieldがあってその2つの文字が同じ場合にButtonを押せるような画面を例とします。

スクリーンショット 2020-04-19 20.46.24.png

ボタンを押したときにTextFieldの値を見張るのではなく、TextFieldの文字をリアルタイムで見張りたいのです。

どうやるか

雑に3つくらい考えられると思います

  • 何も考えずコメントでinputやoutputとか書いてみる例
  • input/outputを型にしてみる例
  • PublishedをやめてSubjectを使う例

何も考えずコメントでinput/outputとか書いてみる例

まず、もう何も考えずにやれるようにやっていく例。こうやってしまうと、@Publishedが増えてinputかoutputか分かりづらい。

import Combine

// 何も考えずコメントでinput/outputとか書いてみる
class ObservableObject1: ObservableObject {
    // input
    @Published var password = ""
    @Published var repeatedPassword = ""

    // output
    @Published var validatedPassword = false

    private var cancellableSet: Set<AnyCancellable> = []

    init() {
        Publishers.CombineLatest($password, $repeatedPassword)
            .map { $0 == $1 }
            .eraseToAnyPublisher()
            .assign(to: \.validatedPassword, on: self)
            .store(in: &cancellableSet)
    }
}

SwiftUI側はこんな感じ

import SwiftUI

struct ContentView: View {
    @ObservedObject var object = ObservableObject1()

    var body: some View {
        VStack {
            TextField("パスワード", text: $object.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("パスワード繰り返して!", text: $object.repeatedPassword)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("押されたよ")
            }) {
                Text("パスワードが一緒だと押せるよ!")
            }
            .disabled(!object.validatedPassword)
        }
    }
}
  • 長所
    • 最初は何も考えないで済む
  • 短所
    • 数が増えると整理されてなさがきつい
    • inputは外から読まれる
    • input用に@Publishedすることで2-way Bindingしてる
      • ObservableObject側で変更しないわけで、@Publishedの意味がない

input/outputを型にしてみる例

これをRxSwift使ってたときのようにちょっと工夫してみる。InputOutput型作ってみる。

import Combine

// InputとOutputをなるべく分けたいので型にした
class ObservableObject2: ObservableObject {
    class Input {
        @Published var password = ""
        @Published var repeatedPassword = ""
    }

    class Output {
        @Published var validatedPassword = false
    }

    // input
    var input: Input
    // output
    var output: Output

    private var cancellableSet: Set<AnyCancellable> = []

    init(input: Input = Input(), output: Output = Output()) {
        self.input = input
        self.output = output

        Publishers.CombineLatest(self.input.$password, self.input.$repeatedPassword)
            .map { $0 == $1 }
            .eraseToAnyPublisher()
            .assign(to: \.validatedPassword, on: self.output)
            .store(in: &cancellableSet)
    }
}

SwiftUI側は次のような感じ

import SwiftUI

struct ContentView2: View {
    @ObservedObject var object = ObservableObject2()

    var body: some View {
        VStack {
            TextField("パスワード", text: $object.input.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("パスワード繰り返して!", text: $object.input.repeatedPassword)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("押されたよ")
            }) {
                Text("パスワードが一緒だと押せるよ!")
            }
            .disabled(!object.output.validatedPassword)
        }
    }
}
  • 長所
    • 型で整理された
  • 短所
    • 単にまとめただけなのでInputの@Publishedの意味がさらによくわからない感じになる
    • inputは外から読まれる

PublishedをやめてSubjectを使う例

とにかくinput側の@Publishedが嫌なのかもしれない。プロパティが更新されたらSubjectを呼び出すようにする。

import Combine

// inputからPublishedをなくせばいい
class ObservableObject3: ObservableObject {

    private let passwordSubject = PassthroughSubject<String, Never>()
    private let repeatedPasswordSubject = PassthroughSubject<String, Never>()

    // MARK: - input

    var password = "" {
        didSet {
            passwordSubject.send(password)
        }
    }

    var repeatedPassword = "" {
        didSet {
            repeatedPasswordSubject.send(repeatedPassword)
        }
    }

    // MARK: - output

    @Published var validatedPassword = false

    // MARK: -

    private var cancellableSet: Set<AnyCancellable> = []

    init() {
        Publishers.CombineLatest(passwordSubject, repeatedPasswordSubject)
            .map { $0 == $1 }
            .eraseToAnyPublisher()
            .assign(to: \.validatedPassword, on: self)
            .store(in: &cancellableSet)
    }
}

SwiftUI側はこんな感じ。最初の例と同じ。

import SwiftUI

struct ContentView3: View {
    @ObservedObject var object = ObservableObject3()

    var body: some View {
        VStack {
            TextField("パスワード", text: $object.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("パスワード繰り返して!", text: $object.repeatedPassword)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("押されたよ")
            }) {
                Text("パスワードが一緒だと押せるよ!")
            }
            .disabled(!object.validatedPassword)
        }
    }
}
  • 長所
    • @Publishedが付いているのは出力だけ
  • 短所
    • 外部からinputは読まれる

まとめ

  • 一番最初の何も考えない例から、最後の@PublishedをやめてSubjectを使う例に移行するのはUI側を変えないので楽
    • 2-way Bindingしないなら出力にのみ@Publishedが付いているほうがコードを把握しやすい
    • しかし結局inputは外部から読まれる(読んで何が困るということはないけども)
  • ちなみにどうあると嬉しいのか
    • プロパティだとinputが外部から読まれる仕組みにならざるを得ない
      • TextFieldからプロパティに書き込むのではなく、ObservableObjectのメソッドを指定して引数付きで呼び出せるのが良い用に思えるが、そんなん考えなくてもプロパティでいいやんという気もする
16
5
1

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
16
5