はじめに
iOS開発でリアクティブといえばRxSwift or Combineかと思いますが、自身RxSwiftばかり触っていたのでCombineを触ってみようと単純に思ったのが動機です。
今後SwiftUI+Combineが主流になった時のことを考え、少しでも慣れておきたいというのも本音です(笑)
さて、今回はTableViewにTodoぽくStringを追加するだけのサンプルです。
UIKitでCombineを使用しました。
下記がリポジトリです↓
ソースコード
import UIKit
import Combine
final class ViewController: UIViewController {
@IBOutlet weak var todoTextField: UITextField!
@IBOutlet weak var addButton: UIButton!
private let cell = "Cell"
private let viewModel: ViewModelType = ViewModel()
private var subscriptions = Set<AnyCancellable>()
@IBOutlet weak var todoTableView: UITableView! {
didSet {
todoTableView.delegate = self
todoTableView.dataSource = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
addButton.addTarget(self, action: #selector(tappedAddButton), for: .touchUpInside)
bindOutput()
}
@objc private func tappedAddButton() {
viewModel.input.addTodo(text: todoTextField.text)
todoTextField.text = ""
}
private func bindOutput() {
viewModel.output.completionSubject.sink { [weak self] completion in
self?.todoTableView.reloadData()
}.store(in: &subscriptions)
viewModel.output.errorSubject.sink { [weak self] error in
let alert = UIAlertController(title: "エラー", message: .none, preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default)
alert.addAction(ok)
self?.present(alert, animated: true)
}.store(in: &subscriptions)
}
}
extension ViewController: UITableViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.output.todoModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cell, for: indexPath)
cell.textLabel?.text = viewModel.output.todoModel[indexPath.row].title
return cell
}
}
import Foundation
import Combine
protocol ViewModelInput {
func addTodo(text: String?)
}
protocol ViewModelOutput {
var todoModel: [TodoModel] { get }
var completionSubject: PassthroughSubject<Void,Never> { get }
var errorSubject: PassthroughSubject<Void,Never> { get }
}
protocol ViewModelType {
var input: ViewModelInput { get }
var output: ViewModelOutput { get }
}
final class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
var input: ViewModelInput { return self }
var output: ViewModelOutput { return self }
// Input
func addTodo(text: String?) {
if text == "" {
errorSubject.send(())
return
}
let todo = TodoModel(title: text)
todoModel.append(todo)
completionSubject.send(())
}
// Output
var todoModel: [TodoModel] = []
var completionSubject = PassthroughSubject<Void, Never>()
var errorSubject = PassthroughSubject<Void, Never>()
}
解説
Combineで言うと
- Publishers -> 発行者
- Subscribers -> 購読者
に大きく分けられます。
今回のサンプルの流れとしては
textFieldに入力
↓
viewModelで処理
↓
値を流す(発行)
↓
Viewで受け取る(購買)
実にシンプルです
ViewModelの処理を見てみましょう
protocol ViewModelOutput {
var todoModel: [TodoModel] { get }
var completionSubject: PassthroughSubject<Void,Never> { get }
var errorSubject: PassthroughSubject<Void,Never> { get }
}
TabelViewに表示する配列データはViewModelで持ちます。
そして成功した時とエラー時に流すPublishersをそれぞれ定義します。
ここででたPassthroughSubject
ですが、RxSwiftでいうところのPublishRelay
にあたり値を保持しないのが特徴です。
逆にCurrentValueSubject
は値を保持し、RxSwiftのBehaviorRelay
にあたります。
@objc private func tappedAddButton() {
viewModel.input.addTodo(text: todoTextField.text)
todoTextField.text = ""
}
// Input
func addTodo(text: String?) {
if text == "" {
errorSubject.send(())
return
}
let todo = TodoModel(title: text)
todoModel.append(todo)
completionSubject.send(())
}
ViewController側でタップするとViewModelで処理が走り、
- 空文字だったらerrorSubjectに値(Void)を流す
- 成功したら配列に追加してcompletionSubjectに値(Void)を流す
ということをやってます。
send
で値を送るの分かりやすいですね👀(RxSwiftでいうaccept
でしょうか)
そしてViewControllerで値を受け取ります。
private func bindOutput() {
viewModel.output.completionSubject.sink { [weak self] completion in
self?.todoTableView.reloadData()
}.store(in: &subscriptions)
viewModel.output.errorSubject.sink { [weak self] error in
let alert = UIAlertController(title: "エラー", message: .none, preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default)
alert.addAction(ok)
self?.present(alert, animated: true)
}.store(in: &subscriptions)
}
.sink
でイベントを購買しています(Rxではsubscribe)
成功したらTableViewをリロード、失敗したらアラートを出します。
.store(in: &subscriptions)
というのがRxSwiftのdisposeBagに相当します。
おわりに
軽く触ってみて、流れはRxSwiftと感覚的に同じですが覚えることが膨大なので少しずつ慣れていこうと思います!