概要
メリークリスマス〜〜〜!あっきーて言いますー!
SwiftでReactive Programmingをしたいとなった際にまず思い浮かぶのはRxSwift
とRxCocoa
になると思います。ただSwiftの純正フレームワークとしてCombineがあり、やはりそれを利用したいと思うようになるわけです。。
そんなわけで今回はCombine
とCombineCocoa
を用いて軽いtableViewの内容表示を行ってみようと思います。
CombineCocoaとは
CombineCocoaはUIKitのPublisherを提供するフレームワークです。ReactiveX
の中だったらRxCocoa
に相当するものになります。使っていく中でRxCocoa
とどういう違いがあるのかとかも見ていけたらいいなて感じです。
実際に触ってく〜
今回デモで作るもの
QiitaAPIを使ってtableView表示をします。tableViewのセルをクリックしたら、SFSafariViewController
によって詳細のwebViewを表示する簡単なものです。俗にいうMVVMチックな構造になってるのでModel、ViewModel、View(ViewController)が存在しています。storyboardも使ってるよ!
APIClientとModel
APIの処理はめんどくさいのでAlamofire使っちゃいました。async、await使ったらescapingとか使わずとももっといい感じになると思います。
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
を設定しておきます。デコードでエラー発生したら面倒臭いので予防線は一応張っておきたい。。
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内での処理は主に
-
Alamofire
を用いて取得した内容をPublisher(CurrentValueSubject)
に格納すること - tableViewのセルを押した際に、
IndexPath
を取得し、SFSafariViewController
で表示する際に使うurlを取得すること
の2つです。
一旦コードを覗いてみましょう。
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
の2つのPublisherが出てくることです。この2つの特徴を軽くまとめると以下の感じです。
特徴 | RxSwiftの中で例えると | |
---|---|---|
CurrentValueSubject | Combine側で値を保持する | BehaviorSubject |
PassthroughSubject | Combine側で値を保持しない | PublishSubject |
CurrentValueSubject
は1つの値をラップして、その値の変更が検知されるたびに通知し、Combine側で値を保持します。
逆にPassthroughSubject
は値を保持しません。そのため、毎回データが1つしかなく、上書きされてしまう場合などに使います。今回はSFSafariViewController
での表示に必要なurlをPublisher
変数に格納しています。tableViewで選択されたセルによって毎回urlは変更されるため、わざわざCombine
で値を保持する必要はないですね!
View
今回は以下のような感じでstoryboardにUIパーツを載せました。
コードはこんな感じです。
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って感じでクオリティ荒いのでこれから修正していきますー!
長々とお付き合いいただきありがとうございました!