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

# 概要
メリークリスマス〜〜〜！あっきーて言いますー！

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

# CombineCocoaとは
[CombineCocoa](https://github.com/CombineCommunity/CombineCocoa)はUIKitのPublisherを提供するフレームワークです。`ReactiveX`の中だったら`RxCocoa`に相当するものになります。使っていく中で`RxCocoa`とどういう違いがあるのかとかも見ていけたらいいなて感じです。

# 実際に触ってく〜
### 今回デモで作るもの
QiitaAPIを使ってtableView表示をします。tableViewのセルをクリックしたら、`SFSafariViewController`によって詳細のwebViewを表示する簡単なものです。俗にいうMVVMチックな構造になってるのでModel、ViewModel、View(ViewController)が存在しています。storyboardも使ってるよ！

<img width="300" alt="スクリーンショット 2022-12-25 22.56.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1500232/fa1b3cbb-b1e6-a488-d366-80e29d0e9941.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を取得すること
の２つです。

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

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

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

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

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

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

### View
今回は以下のような感じでstoryboardにUIパーツを載せました。
<img width="300" alt="スクリーンショット 2022-12-25 22.56.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1500232/fa1b3cbb-b1e6-a488-d366-80e29d0e9941.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`みたいなやつ)。

今回は[こちら](https://stackoverflow.com/questions/66983922/bind-uitableview-with-combine-datasource)の記事をもとにextensionを作りバインディングを行いました。ありがとうございます。

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

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

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

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

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