20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Swift】ゼロからのCombineフレームワーク - UIKitでデータバインディング

Last updated at Posted at 2020-07-31

意外と難しかったCombine

RxSwiftならある程度使えるし、簡単に使えるようになるかな?と思っていました。
しかし、いざ試してみるとなかなか苦労したので、まずは普段の開発に取り入れられる部分から切り出してみました。

UIKitに値をバインドする

import Combine
import UIKit

class ViewModel {
    @Published var labelText: String = "Default value."
}

class ViewController: UIViewController {
    var label: UILabel! // 適当な場所(storyboard, viewDidLoadなど)で初期化する
    let viewModel = ViewModel()
    
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.$labelText
            .map({ Optional($0) })
            .receive(on: DispatchQueue.main)
            .assign(to: \UILabel.text, on: label)
            .store(in: &cancellables)
    }
}

適当なタイミングで、viewModel.labelText = "Hello!"などと値を変更すると、自動的にUILabelが更新されます。

解説

@PublishedPublisherプロトコル

バインドしたいプロパティの変更を監視するために、import Combineしたうえで、@PublishedというProperty Wrapperをバインドしたいプロパティに付与します。

ViewModel
class ViewModel {
    @Published var labelText: String = "Default value."
}

Property Wrapperを付与したプロパティをもつクラスには、自動的に$labelTextのようにプロパティ名の先頭に$がついたgetterが生成されます。@Publishedを付与した場合、このgetterはPublished<Output>.Publisher型になります。

Publisherというのが監視される側のプロトコルです(RxSwiftならObservable)。また、この場合Output = Stringです。

バインド

Publisherに準拠したプロパティをUIViewにバインドするには、assignメソッドを使います。

viewModel.$labelText
    .map({ Optional($0) })
    .receive(on: DispatchQueue.main)
    .assign(to: \UILabel.text, on: label)
    .store(in: &cancellables)

assign

assignメソッドの定義は次のようになっています。

public func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable

keyPathには\UILabel.text1のようにバインド先のUIViewの型名とそのプロパティ名を記載します。

assignメソッドはAnyCancellableというCancellableプロトコルに準拠したクラスのインスタンスを返します。このインスタンスのcancelメソッドを呼ぶことでバインド(監視)が解除されます。

また、AnyCancellableのインスタンスはメモリから解放されるタイミングでもcancelメソッドを呼んでバインドを解除するようです2。そのため、ViewControllerSet<AnyCancellable>型のプロパティを持たせて参照を保持します。追加する際にチェーンでメソッドを呼べるようにstoreメソッドが用意されています。

map

Publisherには様々なオペレーターが用意されており、監視している値に変更を加えたり、流れてくる時間や回数などの条件によって流れをせきとめたりすることができます。

mapはもっとも基本的なオペレーターで、流れてきた値を加工することができます。
ここでは、UILabel.text: String?の型に合わせて、ViewModel.labelText: StringOptional型に変換しています。

receive(on: DispatchQueue.main)

receive(on:)はメソッドチェーンのうちで、このメソッド以降のチェーン内の処理が行われるスレッドを指定することができます。

assignメソッドはUIの更新処理にあたるので、メインスレッドを指定しています。

おまけ:SwiftUI版

import Combine
import SwiftUI

class ViewModel: ObservableObject {
    @Published var labelText: String = ""
}

struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                Text(viewModel.labelText)
                TextField("Title", text: $viewModel.labelText)
                    .multilineTextAlignment(.center)
            }
        }
    }
}

解説

@ObservedObject/ObservableObject

SwiftUIのViewにバインドするには、バインドしたいプロパティをもつviewModel@ObservedObjectを付与します。
ObservableObjectプロトコルに準拠させることで、@ObservedObjectが付与できるようになります。)

struct ContentView: View {
    @ObservedObject private var viewModel = ViewModel()
    ...
}

class ViewModel: ObservableObject {
    @Published var labelText: String = ""
}

バインド

var body: some View {
    NavigationView {
        VStack {
            Text(viewModel.labelText)
            TextField("Title", text: $viewModel.labelText)
                .multilineTextAlignment(.center)
        }
    }
}

@ObservedObjectを付与したプロパティはViewに監視されるようになり、値の変更があればUIが自動更新されます。
そのため、通常のString型を引数にとるクラスではviewModel.labelTextを渡すだけでバインド完了です。

@ObservedObjectによって生成された$viewModelプロパティのlabelTextBinding<String>という型を返します。これをBinding<String>型を引数にとるTextFieldに渡すことによって、双方向バインディングとなります。

こちらのSwiftUI版では、viewModel.labelTextの値を変更するとTextTextFieldの値が更新されますし、キーボードからTextFieldを変更しても、viewModel.labelText(とText)の値が更新されます。

  1. バックスラッシュはKey-Pathを表現するときの記法です。ドキュメントはこちら Key-Path Expression

  2. ドキュメントに記述を見つけられなかったのですが、参照を保持しなかった場合はviewDidLoadを抜けた時点でcancelが呼ばれていました。

20
13
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
20
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?