iOS
MVVM
Swift
RxSwift

【RxSwift+MVVM入門】チャット風アプリのハンズオン Swift4

なぜこの記事を書いたか

「RxSwift」「MVVM」で開発する事になり色々調べたがSwift4でシンプルなハンズオンがあまり無いように感じたので今回まとめました。

<今回作るアプリ>
・シンプルなチャット風UI
・上までスクロールすると自動的に前のデータをロード(無限ロード)
・テキストに入力して「send」ボタンタップでタイムラインにメッセージを追加
・LINE風のテキスト入力UI(テキストがキーボードに隠れないよう制御)

<ソースコード>
https://github.com/okamok/RxSwiftMvvmSample

下記アプリを実際に動かしたい場合に実行して下さい。

1.プロジェクトルートで下記を実行
$ pod install

2.作成された RxSwiftMvvmSample.xcworkspace からプロジェクトを開く

3.Xcode で Run を実行すると動作します。

この記事でカバーされない事

「RxSwift」「MVVM」の詳細な仕様や概念については別に詳しい記事が沢山あるのでそちらを参照お願いします。
今回は最低限、下記の認識で進めたいと思います。

RxSwiftとは?

1.ReactiveX(Reactive Extensions)のSwift実装
2.非同期処理、イベント処理をサポートするライブラリ(=コールバック地獄から解放されます)

MVVMとは?

1.アプリを実装をModel,ViewModel,Viewのレイヤーに分けるアーキテクチャ。
2.View -> ViewModel -> Model と参照する。ViewからはModelに直接アクセスしない。
3.ViewとViewModelをデータバインドさせる。これはデータに変更があった時にUIの表示を変更する場合に必要。
4.上記3つのレイヤーに責務を切り分ける事でViewに責務が集中するのを避ける。(Fat ViewController対策)

ハンズオン

・まずxcodeでSingl View Appの新規プロジェクト「RxSwiftMvvmSample」を作成。
podfileを作成。
https://github.com/okamok/RxSwiftMvvmSample/blob/master/Podfile
下記の通りRxSwift関連のライブラリが含まれている。

Podfile
# RxSwift
pod 'RxSwift',    '~> 4.0'
pod 'RxCocoa',    '~> 4.0'
pod 'RxGesture'
pod 'RxDataSources', '~> 3.0'
pod 'RxKeyboard'

$ pod installを実行

・作成された RxSwiftMvvmSample.xcworkspace からプロジェクトを開く

・MVVMに沿って実装する為、Classesフォルダの下にModel,View,ViewModel,それぞれのフォルダを作成。
スクリーンショット 2018-05-31 18.37.14.png

Viewを作成

Viewフォルダ配下にChatControllerView.swiftを作成。
こちらのソースをコピーします。
https://github.com/okamok/RxSwiftMvvmSample/blob/master/RxSwiftMvvmSample/Classes/View/ChatControllerView.swift

下記ChatControllerView.swift内のRxSwiftとMVVMに関する部分の解説です。

各種Rxをimportする。

ChatControllerView.swift
import RxSwift
import RxCocoa
import RxKeyboard
import RxGesture

ViewModelをインスタンス化する。(Modelはインスタンス化しない)

ChatControllerView.swift
    private let chatViewMoel = ChatViewModel()

setRxSwift functionでRxSwiftの実装を行なっています。

長くなるので下記リンクから参照下さい。

https://github.com/okamok/RxSwiftMvvmSample/blob/master/RxSwiftMvvmSample/Classes/View/ChatControllerView.swift#L89-L202

RxSwiftで下記の内容を実装しています。
・TableViewのデータバインド
・データソースをobserveして変更があった時、自動的に指定したプログラムが動作させる。
・スクロールをobserveして上までいったら過去データをロード。ViewModelのscrollEndComingとBindしている。
・キーボードでテキストが隠れないように制御
・送信ボタン タップ時の処理

ViewModelを作成

ViewModelフォルダ配下にChatViewModel.swiftを作成。
こちらのソースをコピーします。
https://github.com/okamok/RxSwiftMvvmSample/blob/master/RxSwiftMvvmSample/Classes/ViewModel/ChatViewModel.swift

下記ChatViewModel.swift内のRxSwiftとMVVMに関する部分の解説です。

Modelをインスタンス化

ChatViewModel.swift
let messageModel = MessageModel()

dataMessageRx データソースを定義(これは入れ物だけ。実態はModel内で管理されている)

ChatViewModel.swift
//TableViewのデータソース(この内容を一覧に表示する)
var dataMessageRx:RxCocoa.BehaviorRelay<[Message]>

scrollEndComingの値を監視して スクロールで上まで行った時の処理。ここでsubscriveして観察している。

ChatViewModel.swift
        //スクロール 上まで到達した時
        // driveはsubscribeと同様、イベントの購読を行う。(=ここで受信する)
        scrollEndComing.asDriver()
            .drive(onNext: { bool in
                if (bool) {
                    print("TOPに達しました")

                    //データソースでいま何件表示しているかを確認して現ページを判定
                    let page:Int = self.dataMessageRx.value.count / 20
                    self.messageModel.currentPage = page + 1   //ページを加算

                    if (self.messageModel.dataMessageRx.value.count > 0) {
                        self.messageModel.data = []   //データ取得前に初期化
                        self.messageModel.messagesGet()
                    }
                }
            })
            .disposed(by: disposeBag)

(参考)Driverの概念
https://qiita.com/inamiy/items/d6fa90d0401fa0e83852

Modelを作成

Modelフォルダ配下にChatModel.swiftを作成。
こちらのソースをコピーします。
https://github.com/okamok/RxSwiftMvvmSample/blob/master/RxSwiftMvvmSample/Classes/Model/ChatModel.swift

下記ChatModel.swift内のRxSwiftとMVVMに関する部分の解説です。

structを定義

ChatModel.swift
//チャット データソースとなるstruct
struct Message {
    let message: String

    init(message:String) {
        self.message = message
    }
}

dataMessageRx データソースを定義 この値に変更があるとViewModelを通してViewのUIが変更される。

ChatModel.swift
    var dataMessageRx = RxCocoa.BehaviorRelay<[Message]>(value: [])  //TableViewのデータソース。Variableがdeprecateになったので BehaviorRelayを使用

データソースを返す

ChatModel.swift
    func messagesRx() -> RxCocoa.BehaviorRelay<[Message]> {
        //データ取得前に現カウントと取得しておいてから初期化
        self.data = []

        self.messagesGet()
        return self.dataMessageRx
    }

ページ数に応じてデータを取得してセット。

ChatModel.swift
    //ページ数に応じてデータを取得
    // 取得後、dataMessageRx のValueを変更する(=subscribeしている箇所を実行)
    func messagesGet() {

        //**** 表示するデータを取得してデータソースを更新(すると自動的にobserveしているプログラムが実行される)
        //      今回は配列だが、本来はFirebaseなどのDBからデータを取得する部分。
        //ページ数を考慮してデータを取得
        self.data = (dataDB?.suffix(20 * currentPage).map { $0 })!

        //取得したデータでobservableを変更
        self.dataMessageRx.accept(self.data!)
    }

データの追加

ChatModel.swift
    func add(msg:String){
        //データソースを更新
        self.dataDB?.append(Message(message: msg))
        //self.data?.append(Message(message: msg))
        self.messagesGet()
        self.dataMessageRx.accept(self.data!)
    }

RxSwiftとは関係ない周辺のファイルを作成と修正

Common/MessageTableViewCell.swift を作成して下さい。
https://github.com/okamok/RxSwiftMvvmSample/blob/master/RxSwiftMvvmSample/Common/MessageTableViewCell.swift

AppDelegate.swiftを下記のように更新して下さい。これはストーリーボードを使いたくない為の修正となります。
https://github.com/okamok/RxSwiftMvvmSample/blob/master/RxSwiftMvvmSample/AppDelegate.swift

完了

これでXcodeでRunすると実際にアプリが動きます。

まとめ

RxSwiftやMVVMについて調べていた時に分かりやすいハンズオンが少ないように感じたので今回まとめました。実際にチャットを作るとなるとFirebaseなどを使用すると思いますので適宜修正して頂ければと思います。
間違え等ありましたらコメント頂けますと幸いです。