3
4

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 1 year has passed since last update.

[Swift]CombineをUIKitで試してみた

Last updated at Posted at 2022-08-27

はじめに

iOS開発でリアクティブといえばRxSwift or Combineかと思いますが、自身RxSwiftばかり触っていたのでCombineを触ってみようと単純に思ったのが動機です。

今後SwiftUI+Combineが主流になった時のことを考え、少しでも慣れておきたいというのも本音です(笑)

さて、今回はTableViewにTodoぽくStringを追加するだけのサンプルです。

UIKitでCombineを使用しました。

下記がリポジトリです↓

ソースコード

ViewController.swift

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
    }
}


ViewModel.swift

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の処理を見てみましょう

ViewModel.swift
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にあたります。

ViewController.swift
@objc private func tappedAddButton() {
        viewModel.input.addTodo(text: todoTextField.text)
        todoTextField.text = ""
    }

ViewModel.swift
   // 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で値を受け取ります。

ViewController.swift
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と感覚的に同じですが覚えることが膨大なので少しずつ慣れていこうと思います!

3
4
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
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?