iOS
ReactiveCocoa
MVVM
Swift
ReactiveSwift

ReactiveSwift / ReactiveCocoa / MVVM 入門 Part 1/2


この記事について

ReactiveCocoa、ReactiveSwiftに関する記事が英語、日本語含め全然見当たらないので、(Rxはたくさんあるのに...)Samplerを作ってみました。2部構成になっています。RAC、MVVM共に完全に初心者なので、間違いがありましたらご指摘どうぞよろしくお願い致しますmm


この記事で作るもの

iTunesのAPIを検索できるよ、といういたってシンプルなアプリをMVVMで作ります。

完成版のソースコードは下記です。

ReactiveOnlineSearching

ReactiveiTunesSearching.gif

以下の記事を参考にしています。


この記事でカバーしないこと

以下についてこの記事で触れません


  • MVVMの概念の詳細な説明、メリデメ

  • FRP(Functional Reactive Programming)の概念の詳細な説明

  • ReactiveSwiftの文法、データ型に関する詳細な説明


MVVMとは?

Model-View-ViewModelというデザインパターン。主にMVCにおけるViewControllerの肥大化しがちという問題を解決できるアーキテクチャである。ReactiveCocoaにおける「バインディング」という概念はMVVMと相性が良いので、ReactiveCocoaを使ったプロジェクトではMVVMが使われることが多い。

(今度MVVMについても書きたい)


ReactiveSwift / ReactiveCocoaとは?


ReactiveSwift

ReactiveSwiftとは、FRP(Functional Reactive Programming)という概念をSwiftに実装したもの。ストリームという概念を使ってデータとコミュニケーションをすることで、delegateやnotification、KVOなど様々あるコールバック処理の方法を一つの方法にまとめてくれるメリットがあります。

(今度ReactiveSwiftの文法についても書きたい)


ReactiveCocoa

ReactiveCocoaとは、ReactiveSwiftを使ってCocoaフレームワークをいじりたいときに使うライブラリです。要するにUIに紐づくFRPの処理を担当します。


実際に作ってみよう

ReactiveOnlineSearching-Sampler

上記からスターターコードをダウンロードしてください。


①Search Barを実装する

まずはSearch BarをReactiveに表示してみましょう。


準備

MVVMを使いますので、まずはModelViewViewModelという三つのグループを追加します。

また、MVVMにおいてViewControllerはViewの役割を果たす のでHomeViewController.swiftTrackCell.swiftをViewの中に入れます。ファイル構造は以下のようになります。

<スクリーンショット> 


ViewModelを作る

それでは、ViewModelを作って行きます。HomeViewModel.swiftをViewModelグループの中に追加し、以下のように記載してください。(あとで必要になるライブラリも全てimportしてあります。)

import Foundation

import ReactiveSwift
import ReactiveCocoa
import Result
import Changeset

class HomeViewModel {
var searchStrings: Signal<String?, NoError>

init(searchStrings: Signal<String?, NoError>) {
self.searchStrings = searchStrings
}
}

ここにおいて、searchStringsをSignal型で実装しています。これは、後々このsearchStringsをObserveすることでModelを更新する ためです。後ほど詳しく解説します。

次に、HomeViewController.swiftに以下のコードを追加してください。(ライブラリのimportを忘れないようにしてください。)

import UIKit

import ReactiveCocoa
import ReactiveSwift

class HomeViewController: UIViewController {

@IBOutlet weak var searchBar: UISearchBar!
@IBOutlet weak var navigationBar: UINavigationBar!
@IBOutlet weak var tableView: UITableView!

private let trackCell = "trackCell"

private var viewModel: HomeViewModel!

override func viewDidLoad() {
super.viewDidLoad()

let searchStrings = searchBar.reactive.continuousTextValues

self.viewModel = HomeViewModel(searchStrings: searchStrings)

}

}

ここにおいて、searchBar.reactive.continuousTextValues とは、searchBarのtextに何からの変更があるたびに、それをSignalとして送るようなプロパティ です。このsignalをViewModel側でObserveして色々検索処理を後々実装していくので、initの引数に渡しておきます。これにより、ViewModel.searchStringsは常にsearchBar.textの変更をSignalとして受け取り ます。

なお、少し脱線しますが、MVVMにおいてUIKitをimportするのはViewのファイルのみです。ViewModelやModelにおいてUIKitは原則importしないので、importしないといけない状況になっていたらそれはデザインパターンのルールを犯していると疑いましょう。


試しにObserveする

それでは、試しに以下のコードをHomeViewModel.swiftinitに追加してみましょう。

self.searchStrings.observeValues { print($0) }

ViewModel上でsearchStringsのSignalがうまくObserveできていれば、searchBarに文字を入力/削除するたびに変更がprintされているはずです。

(確認ができたら、この一行のコードは使わないので消してしまって大丈夫です。)


バインディングする

以下の一行のコードをHomeViewController.swiftviewDidLoadに書いてください

  if let navItem = self.navigationBar.topItem {

navItem.reactive.title <~ viewModel.searchStrings
}

ここにおいてUIコンポーネントであるnavigationBarのtextは、ReactiveCocoaの機能を使って viewModel.searchStringsと バインディング されています。<~オペレータはバインディングをするオペレータですが、使うにはReactiveSwiftをimportする必要があります。バインディングすることで、navigationBarのtextは常にHomeViewModel.searchStringsと同値になります。 つまり、HomeViewModel.searchStringsに何かしらの変更があった場合にnavigationBar.textははそれを反映します。

ここでコードを実行してみると、SearchBarに打った文章がTitleBarにも出力されていることがわかります。これを整理すると以下のようになります。


  1. SearchBarにTextをinput

  2. HomeViewModelのsearchStringsが更新される

  3. HomeViewModelのsearchStringsの更新をnavigationBarのtextが受け取り、値を変更

ポイントは、ViewModelは、Viewの出力については何も知らない ということです。ViewはViewModelを参照しますが、ViewModelはViewに対してポインタを持っていません。ReactiveCocoaを使うことで、簡単にMVVMの実装ができることがわかります。


②iTunes APIを使ったサーチ機能を実装する

ReactiveSwiftを使ったネットワークリクエスト処理を書いていきます。なお、この部分はReactiveCocoaのDocumentationにあるExampleを参考にしました。


Modelを作る

iTunesから受け取ったデータをTrackクラスで管理します。Track.swiftを作り、以下のコードを書いてModelの中に保存しましょう。

(Equatableプロトコルを継承して==オペレータを実装していますが、これはPart2で必要になります。)

import Foundation

import ReactiveSwift

class Track: Equatable {

let trackName: String
let artistName: String
let index: Int

init(dict: Dictionary<String, Any>) {
self.trackName = dict["trackName"] as! String
self.artistName = dict["artistName"] as! String
self.index = 0
}

static func == (lhs: Track, rhs: Track) -> Bool {
if lhs.trackName == rhs.trackName, lhs.artistName == rhs.artistName {
return true
} else {
return false
}
}

}

また、ViewModelに以下を記載します。

class HomeViewModel {

private var tracks: [Track]
var searchStrings: Signal<String?, NoError>

init(searchStrings: Signal<String?, NoError>) {
self.tracks = []
self.searchStrings = searchStrings

let searchResults = searchStrings
.flatMap(.latest) { (query: String?) -> SignalProducer<(Data, URLResponse), AnyError> in
let request = self.makeSearchRequest(escapedQuery: query)
return URLSession.shared.reactive
.data(with: request!)
.retry(upTo: 2)
.flatMapError { error in
print("Network error occurred: \(error)")
return SignalProducer.empty
}
}
.map { (data, response) -> [Track] in
return self.searchResults(from: data)
}
.observe(on: UIScheduler())

searchResults.observe { event in
switch event {
case let .value(results):
self.tracks = results
print(self.tracks.count)
print(self.tracks.first?.artistName)
case let .failed(error):
print("Search error: \(error)")

case .completed, .interrupted:
break
}
}
}

private func makeSearchRequest(escapedQuery: String?) -> URLRequest? {
if var urlComponents = URLComponents(string: "https://itunes.apple.com/search"), let escapedQuery = escapedQuery {
urlComponents.query = "media=music&entity=song&term=\(escapedQuery)"
guard let url = urlComponents.url else { return nil }

return URLRequest(url: url)
} else {
return nil
}

}

private func searchResults(from json: Data) -> [Track] {
var resultTracks = [Track]()

do {
guard let json = try JSONSerialization.jsonObject(with: json, options: []) as? [String: Any] else {return []}

if let results = json["results"] as? [Dictionary<String, Any>] {
_ = results.map({ dict in
let track = Track(dict: dict)
resultTracks.append(track)
})
}

} catch let jsonErr {
print("Error serializing json: ", jsonErr)
}

return resultTracks
}
}

このコードをRunしてSearchBarに文字を打ってみましょう。searchBarの文字列から曲のデータが取得できた場合は、取得された曲の数と、最初の曲のアーティスト名がprintされるはずです。

上において、



  • searchStringsのSignalを受け取って、データのリクエストを送り、帰ってきたjsonを[Track]の形にして返すということをしています。

  • リクエストを送るときに、データが取得できなかった場合は.retryでもう2回リクエストを送り、それでもデータが所得できない場合はSignalProducer.emptyを送ることでストリームが途中で途絶えないようにしています。

  • 上記のSignalをsearchResultに格納します。それをObserveして、そのeventの型ごとに処理をしています

なお以下二つのヘルパーファンクションの役割はシンプルです。



  • makeSearchRequestはクエリに使用するStringを受け取ってAPIのリクエストを返す


  • searchResultsはjsonを[Track]のArrayにして返す


まとめ

今回は


  • SearchBarからのinputを元にNavigationBarをReactiveに表示する

  • SearchBarからのinputを元にiTuneAPIを叩いて結果をprintする

というところをやりました。

コメント、ご指摘お待ちしておりますmm

第2回目の記事はこちら↓↓

ReactiveSwift / ReactiveCocoa / MVVM 入門 Part 2/2


参考