1
1

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.

Life is Tech ! KantoAdvent Calendar 2022

Day 25

UIKitでもCombineを使ってReactiveにやってみたい!

Posted at

概要

メリークリスマス〜〜〜!あっきーて言いますー!

SwiftでReactive Programmingをしたいとなった際にまず思い浮かぶのはRxSwiftRxCocoaになると思います。ただSwiftの純正フレームワークとしてCombineがあり、やはりそれを利用したいと思うようになるわけです。。
そんなわけで今回はCombineCombineCocoaを用いて軽いtableViewの内容表示を行ってみようと思います。

CombineCocoaとは

CombineCocoaはUIKitのPublisherを提供するフレームワークです。ReactiveXの中だったらRxCocoaに相当するものになります。使っていく中でRxCocoaとどういう違いがあるのかとかも見ていけたらいいなて感じです。

実際に触ってく〜

今回デモで作るもの

QiitaAPIを使ってtableView表示をします。tableViewのセルをクリックしたら、SFSafariViewControllerによって詳細のwebViewを表示する簡単なものです。俗にいうMVVMチックな構造になってるのでModel、ViewModel、View(ViewController)が存在しています。storyboardも使ってるよ!

スクリーンショット 2022-12-25 22.56.55.png

APIClientとModel

APIの処理はめんどくさいのでAlamofire使っちゃいました。async、await使ったらescapingとか使わずとももっといい感じになると思います。

APIClient.swift
import Foundation
import Alamofire
struct APIClient{
    static let shared = APIClient()
    func getArticles(query: String, completion: @escaping(Result<[DataModel],Error>)->Void){
        let url: String = "https://qiita.com/api/v2/items?page=1&query=tag%3A\(query)"
        let encodeURL = url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
        AF.request(encodeURL)
            .responseDecodable(of: DataModel.self) { response in
                switch response.result{
                case .success:
                    guard let data = response.data else {
                        return
                    }
                    do {
                        let articles = try JSONDecoder().decode([DataModel].self, from: data)
                        completion(.success(articles))
                    } catch {
                        completion(.failure(error))
                        print("decode error")
                    }
                case .failure:
                    print("error: \(response.result)")
                }
            }
    }
}

モデルについては一応CodingKeyを設定しておきます。デコードでエラー発生したら面倒臭いので予防線は一応張っておきたい。。

DataModel.swift
import Foundation
struct DataModel: Codable{
    
    var title: String
    var user: User
    var urlName: String
    
    struct User: Codable {
        var name: String
    }
    
    enum CodingKeys: String, CodingKey{
        case title = "title"
        case user = "user"
        case urlName = "url_name"
    }
}

ViewModel

ViewModel内での処理は主に

  1. Alamofireを用いて取得した内容をPublisher(CurrentValueSubject)に格納すること
  2. tableViewのセルを押した際に、IndexPathを取得し、SFSafariViewControllerで表示する際に使うurlを取得すること
    の2つです。

一旦コードを覗いてみましょう。

ArticleListViewModel.swift
final class ArticleListViewModel{
    static let shared = ArticleListViewModel()
    private let apiClient = APIClient.shared
    var articleListSubject = CurrentValueSubject<[DataModel], Never>([])
    var articleDetailSubject = PassthroughSubject<URL, Never>()
    
    private func setList(list: [DataModel]){
        self.articleListSubject.send(list)
    }
    
    //MARK: HTTPリクエストを行ない、Publisherに値を格納
    func fetchArticleData(query: String){
        apiClient.getArticles(query: query){ response in
            switch response{
            case .success(let data):
                self.setList(list: data)
                break
            case .failure(let error):
                print("decodeエラー:\(error)")
                break
            }
        }
    }
    
    //MARK: 詳細に遷移するときの操作
    func handleDetailData(indexPath: IndexPath){
        let item = articleListSubject.value[indexPath.row]
        guard let url = URL(string: item.urlName) else {return}
        articleDetailSubject.send(url)
    }
}

ここで面倒臭いのがCurrentValueSubjectPassthroughSubjectの2つのPublisherが出てくることです。この2つの特徴を軽くまとめると以下の感じです。

特徴 RxSwiftの中で例えると
CurrentValueSubject Combine側で値を保持する BehaviorSubject
PassthroughSubject Combine側で値を保持しない PublishSubject

CurrentValueSubjectは1つの値をラップして、その値の変更が検知されるたびに通知し、Combine側で値を保持します。

逆にPassthroughSubject値を保持しません。そのため、毎回データが1つしかなく、上書きされてしまう場合などに使います。今回はSFSafariViewControllerでの表示に必要なurlをPublisher変数に格納しています。tableViewで選択されたセルによって毎回urlは変更されるため、わざわざCombineで値を保持する必要はないですね!

View

今回は以下のような感じでstoryboardにUIパーツを載せました。
スクリーンショット 2022-12-25 22.56.55.png

コードはこんな感じです。

ViewController.swift
class ViewController: UIViewController, UITableViewDelegate{
    private let viewModel = ArticleListViewModel.shared
    private var cancellables = Set<AnyCancellable>()
    
    @IBOutlet weak var tableView: UITableView!{
        didSet{
            tableView.delegate = self
            tableView.registerForCell(TableViewCell.self)
            viewModel.articleListSubject.sink(receiveValue: tableView.items({ tableView, indexPath, item in
                let cell = tableView.dequeueCellForIndexPath(indexPath) as! TableViewCell
                cell.renderingCell(model: item)
                return cell
            }))
            .store(in: &cancellables)
            
            tableView.didSelectRowPublisher
                .sink { indexPath in
                    self.viewModel.handleDetailData(indexPath: indexPath)
                }
                .store(in: &cancellables)
        }
    }
    
    @IBOutlet weak var searchBar: UISearchBar!{
        didSet{
            searchBar.delegate = self
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}

extension ViewController: UISearchBarDelegate{
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        self.viewModel.fetchArticleData(query: searchBar.text!)
    }
}

今回tableViewの設定で
viewModel.articleListSubject.sink(receiveValue: tableView.items({ tableView, indexPath, item in ~}というコードを用いました。CombineCocoaではtableViewのデータバインディングの機能はまだ搭載されていない模様です(tableView.rx.itemsみたいなやつ)。

今回はこちらの記事をもとにextensionを作りバインディングを行いました。ありがとうございます。

didSelectRowPublisherは搭載されていたのでそのまま利用してます👍

searchBarで文字入力後検索ボタンを押したときに毎回記事が取得できるようにしてます!

使ってみた感じ

ちょっとCombineCocoaを使ってみた感じtableViewでデータバインディングはできると便利なのになあとは思いました。やはりRx系列のものは機能が充実してます💦

ただアプリの機能自体が大きくなってきた場合にはCombineCocoaは必須級になってくると思います。簡単なボタンなどのPublisherなどは充実していますし、Combineを先にいじってしまった身としてはかなりスムーズに入ることができたかなと思います。SwiftUIでCombineをいじった後にこのCombineCocoaで学習を始めるのも悪くないかなって思ってます!

done is better than perfectって感じでクオリティ荒いのでこれから修正していきますー!
長々とお付き合いいただきありがとうございました!

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?