RxSwiftを投げ出したあなたに贈る、ReduxライクなReSwiftのご提案

# はじめに

サイバー・バズの@Hakotaです。

弊社の@yuinchirnが執筆した
[自社サービス採用へ。本気で作るReact+Reduxのプロジェクト構成][1]で紹介した
プロジェクトにメンバーとして参加しています。

巷ではiOSアプリをRxSwiftで実装するのが流行りのようですが
スマホアプリにもReduxの思想を持ち込むことでよりスケーラブルでテスタブルな実装が可能です。

## そもそもReduxって何？

僕もReact.js、Redux、Javascript、ES6がわからない状態で開発に入ったので苦労しました。
わからないなりにスライドにまとめてみたので、参考がてら見てもらえると雰囲気は掴んでもらえるかと思います。

[Reduxをレストランで例えると理解が捗った件][2]

## ざっくり説明
- **AcitonCreator**
適切なActionを生成する。

- **Aciton**
トリガー兼Stateに持たせたい情報のオブジェクト

- **Middleware**
Reducerの実行前後に処理を追加するための仕組みです

- **Reducer**
Actionの値をもとに、Stateを更新する

- **Store**
アプリケーションのState(状態)を管理

# 作ったもの

Qiitaクライアントアプリ**『[Qiitenus][7]』**をサンプルとして作りました。
随時更新してきます。

![Icon-App-60x60@2x.png](https://qiita-image-store.s3.amazonaws.com/0/60014/33df9c23-5280-6752-a1e8-e805463d8aee.png)


## 環境

- Xcode8
- Swift 3.0
- carthage 0.17.2

## ライブラリ


```Cartfile.

github "ReSwift/ReSwift" "2.1.0"
github "onevcat/Kingfisher" "3.0.1"
github "Hearst-DD/ObjectMapper" "2.0.0"
github "Alamofire/Alamofire" "4.0.0"
github "ReactKit/SwiftTask" "swift/3.0"
github "SVProgressHUD/SVProgressHUD"
```

# 実装

Qiitaの新着投稿を取得する実装部分をつかって解説をしたいと思います。

## 全体構成

```
├── project
│   ├── View
│   ├── API (各種Requestを追加していきます)
│   ├── Controller
│   ├── Extentions
│   ├── Model (ObjectMapperを使ったモデル群)
│   ├── ReSwift
│   │   ├── Actions (各種Actionを追加していきます)
│   │   ├── Middleware (各種Middleware)
│   │   ├── Reducers (各種Reducer)
│   │   ├── States (各種State)
│   │   ├── ConfigureStore.swift
│   │   └── RootReducers.swift
│   │
│   └── AppDelegate.swift
```

## Model

新着投稿のフィードをテーブルに表示するため記事用のモデルを作ります。

```Article.swift

import ObjectMapper

struct Article {
    var id: String?
    var isPrivate: Bool?
    var title: String?
    var body: String?
    var renderedBody: String?
    var url: String?
    var tags: [Tag]?
    var user: User?
    var coEditing: Bool?
    var createdAt: String?
    var updatedAt: String?
}

extension Article: Equatable {}

func ==(lhs: Article, rhs: Article) -> Bool {
    return lhs.id == rhs.id
}

extension Article: Mappable {
    
    init?(map: Map) {
        //        mapping(map)
    }
    
    mutating func mapping(map: Map) {
        id           <- map["id"]
        isPrivate    <- map["private"]
        title        <- map["title"]
        body         <- map["body"]
        renderedBody <- map["rendered_body"]
        url          <- map["url"]
        tags         <- map["tags"]
        user         <- map["user"]
        coEditing    <- map["coediting"]
        createdAt    <- map["created_at"]
        updatedAt    <- map["updated_at"]
    }
}
```

```Tag.swift

import ObjectMapper

struct Tag {

    var name: String?
    var versions: [String]?

}

extension Tag: Mappable {

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        name <- map["name"]
        versions <- map["versions"]
    }
    
}
```

```User.swift


import ObjectMapper

struct User {
    var permanentId: String?
    var id: String?
    var name: String?
    var location: String?
    var profileImageUrl: String?
    var twitterScreenName: String?
    var facebookId: String?
    var linkedInId: String?
    var githubAccountName: String?
    var itemsCount: Int?
    var followeesCount: Int?
    var followersCount: Int?
    var organization: String?
    var websiteUrl: String?
    var description: String?
}

extension User: Mappable {

    init?(map: Map) {}

    mutating func mapping(map: Map) {
        permanentId       <- map["permanent_id"]
        id                <- map["id"]
        name              <- map["name"]
        location          <- map["location"]
        profileImageUrl   <- map["profile_image_url"]
        twitterScreenName <- map["twitter_screen_name"]
        facebookId        <- map["facebook_id"]
        linkedInId        <- map["linkedin_id"]
        githubAccountName <- map["github_login_name"]
        itemsCount        <- map["items_count"]
        followeesCount    <- map["followees_count"]
        followersCount    <- map["followers_count"]
        organization      <- map["organization"]
        websiteUrl        <- map["website_url"]
        description       <- map["description"]
    }

}

extension User {
    func fetchUserName() -> String {
        return name ?? ""
    }
}
```

## API

APIKitのようなAPIクライアントを実装していきます。
こちらを参考させていただきました。
[Alamofire + SwiftTask + ObjectMapperでAPIKitのようなAPIClientを実装する][6]

```QiitaRequest.swift

import UIKit
import ObjectMapper
import Alamofire

protocol QiitaRequestType {

    associatedtype Response
    var baseURL : URL { get }
    var method : HTTPMethod { get }
    var path : String { get }
    var parameters : [String : AnyObject] { get }
    var HTTPHeaderFields : [String : String] { get }

    func responseFromObject(_ object: AnyObject, URLResponse: HTTPURLResponse) -> Response?
    func errorFromObject(_ object: AnyObject, URLResponse: HTTPURLResponse) -> Error?

}

extension QiitaRequestType {

    var baseURL: URL {
        return URL(string: "https://qiita.com/api/v2")!
    }

    var HTTPHeaderFields : [String : String] {
        return [:]
    }

    func errorFromObject(_ object: AnyObject, URLResponse: HTTPURLResponse) -> Error? {
        return nil
    }
    
}

extension QiitaRequestType where Response: Mappable {
    func responseFromObject(_ object: AnyObject, URLResponse: HTTPURLResponse) -> Response? {
        guard let model = Mapper<Response>().map(JSONObject: object) else {
            return nil
        }
        return model
    }
}
```

```FeedRequest.swift
import UIKit
import ObjectMapper
import Alamofire

class FeedRequest {
    /**
     *  新着フィードを取得する
     */
    struct GetNewFeed: QiitaRequestType {
        
        var page : Int = 1
        var perPage : Int = 10

        typealias ResponseComponent = Article
        typealias Response = [ResponseComponent]

        var path: String {
            return "/items"
        }

        var method: HTTPMethod {
            return .get
        }

        var parameters : [String : AnyObject] {
            return [
                "page"      : self.page as AnyObject,
                "per_page"  : self.perPage as AnyObject
            ]
        }

        var HTTPHeaderFields : [String : String] {
            return [:]
        }

        func responseFromObject(_ object: AnyObject, URLResponse: HTTPURLResponse) -> Response? {
            
            guard let model = Mapper<ResponseComponent>().mapArray(JSONObject: object) else {
                return nil
            }
            
            return model
        }
    }
}
```

前述のスライドに登場したActionCreatorをAPIクライアントとして作成します。

```ActionCreator.swift

import Alamofire
import SwiftTask
import ObjectMapper

public enum APIError : Error {
    case connectionError(NSError)
    case invalidResponse(AnyObject?)
    case parseError(AnyObject?)
}

struct ActionCreator {
    
    static func callApi<T : QiitaRequestType>(_ request : T) -> Task<Float, T.Response, APIError> {
        
        let endPoint: String            = request.baseURL.absoluteString+request.path
        let params: [String : Any]      = request.parameters
        let headers: [String:String]    = request.HTTPHeaderFields
        let method: HTTPMethod          = request.method
        let encoding: ParameterEncoding = (method == .get) ? URLEncoding.default : JSONEncoding.default
        
        let task = Task<Float, T.Response, APIError> { progress, fulfill, reject, configure in
            let req = Alamofire.request(endPoint, method: method, parameters: params, encoding: encoding, headers: headers)
                .validate { request, response, data in
                    return .success
                }
                .responseJSON { response in
                    debugPrint(response)
                    if let error = response.result.error {
                        reject(.connectionError(error as NSError))
                        return
                    }
                    if let object = response.result.value, let URLResponse = response.response {
                        guard let model: T.Response = request.responseFromObject(object as AnyObject, URLResponse: URLResponse) else {
                            reject(.parseError(object as AnyObject?))
                            return
                        }
                        fulfill(model)
                    }else {
                        reject(.invalidResponse(nil))
                    }
            }
            
            print(req)
            print(req.debugDescription)
        }
        return task
    }
}
```
## Action

いよいよReSwiftの実装部分に入ります。
Feed用のActionを作ります。
APIからのResponseを必要とするActionには、responseを持たせてあげます。
Actionだけstructの名前を大文字スネークケースで記述してます。

```FeedAction.swift

import ReSwift

/**
 *  Feedを取得
 */
struct GET_NEW_FEED: Action {
    var response: Array<Article>?
}
```

## Middleware

今回使用した感じだと、特に必要性を感じなかったMiddlewareもとりあえず実装しました。
Requestの結果を元に更に異なったRequestを出したい場合に、こちらに次のRequestを書いておくとネストが深くなることがなく便利です。

```FeedMiddleware.swift

import ReSwift

let FeedMiddleware: Middleware = { dispatch, getState in
    return { next in
        return { action in
            switch action {
            case is GET_NEW_FEED:
                print("MMMMMMMMMMMMMM")
                print("GET_NEW_FEED")
                print("MMMMMMMMMMMMMM")
                break
            default:
                break
            }
            return next(action)
        }
    }
}
```

## State

Feed関連の状態を管理するStateを作ります。
ページャー用にpageNumberをもたせたり
refresh用にフラグをもたせたり
記事の配列をもたせたり…。

```
import ReSwift

struct FeedState: StateType {
    var pageNumber: Int = 1
    var articleList: [Article]?
    var isRefresh: Bool = false
}
```

AppStateという名前でアプリのStateをつくります。
AppState内に各種Stateを追加し、AppStateを観測をすることで
各種Stateを取得できるようにします。

```AppState.swift
import ReSwift

struct AppState: StateType {
    var feedState = FeedState()
}
```


## Reducer

Reducerで先程のStateにもたせた、articleListに対して
GET_NEW_FEEDが持っているResponseを渡します。

```FeedReducer.swift
import ReSwift

struct FeedReducer: Reducer {
    
    typealias ReducerStateType = AppState
    
    public func handleAction(action: Action, state: AppState?) -> AppState {
        var state = state ?? AppState()

        switch action {
        case let act as GET_NEW_FEED :
            print("RRRRRRRRRRRRRR")
            print("GET_NEW_FEED")
            print("RRRRRRRRRRRRRR")
            state.feedState.articleList = act.response
            break
        default:
            break
        }
        return state
    }
}
```

## Conf

```RootReducers.swift

import ReSwift

var rootReducers = CombinedReducer([
    FeedReducer()
    ])
```

appStoreに対して、今まで設定してきた内容を渡してあげます。
**※rootReducersにReducerを追加し忘れると、動作はするもののMiddlewareやReducerに入ってこず、Stateの値が変わらないので注意です。**

```ConfigureStore.swift

import ReSwift

var appStore = Store(
    reducer:rootReducers,
    state: AppState(),
    middleware: [
        FeedMiddleware
    ])
```


## Controller

ControllerでStateを観測し、Stateが持っている値を利用して実装していきます。

```FeedViewController.swift

import UIKit
import ReSwift
import Kingfisher

class FeedViewController: UIViewController, StoreSubscriber {

　　// 注意　AppStateを選択すること
    typealias StoreSubscriberStateType = AppState

    @IBOutlet weak var feedTable: UITableView!

    var feedList: [Article]?

    override func viewDidLoad() {

        super.viewDidLoad()
        feedTable.delegate = self
        feedTable.dataSource = self
        loadContents()
    }

    override func viewWillAppear(_ animated: Bool) {
        // appStoreの観測開始
        appStore.subscribe(self)
    }

    override func viewWillDisappear(_ animated: Bool) {
        // appStoreの観測を停止
        appStore.unsubscribe(self)
    }

    /**
     reactのライフサイクルにあるcomponentWillReceivePropsのようなもの
　　　Actionが発行されて、stateが更新されると呼ばれる
     - parameter state: 更新されたstate
     */
    func newState(state: AppState) {
         if state.feedState.articleList != nil {
             feedList = state.feedState.articleList
             feedTable.reloadData()
         }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func loadContents() {
        // ActionCreatorでRequestを指定し、結果を元にAction発行
        ActionCreator.callApi(FeedRequest.GetNewFeed(page: 1, perPage: 10))
            .success{ result in
                // 新着のFeed情報のActionを発行
                appStore.dispatch(GET_NEW_FEED(response: result))
            }.failure{ error in
                print("error:", error)
        }
    }
}
```

同ファイル内で、Extensionで分けてTableを管理します。
ViewにはCustomCellとして、ArticleCellを作成しました

```ArticleCell.swift

import UIKit

class ArticleCell: UITableViewCell {
    
    @IBOutlet weak var userNameLabel: UILabel!
    @IBOutlet weak var profileImage: UIImageView!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var tagLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

}
```

```FeedViewController.swift

extension FeedViewController: UITableViewDelegate, UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return feedList != nil ? feedList!.count : 0
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 160
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: ArticleCell = tableView.dequeueReusableCell(withIdentifier: "ArticleCell", for: indexPath) as! ArticleCell

        // feedListが値を持ってる場合
        if feedList != nil {
            let url = URL(string: feedList![(indexPath as NSIndexPath).row].user!.profileImageUrl!)
            cell.profileImage.kf_setImage(with: url)
            cell.userNameLabel.text = feedList![(indexPath as NSIndexPath).row].user!.id
            cell.titleLabel.text = feedList![(indexPath as NSIndexPath).row].title

            var tagValues: String! = ""
            for tag in feedList![(indexPath as NSIndexPath).row].tags! {
                tagValues.append(tag.name!)
                tagValues.append(",")
            }
            cell.tagLabel.text = tagValues
        }
        return cell
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "WebViewC") as! WebViewC
        vc.urlStr = feedList![(indexPath as NSIndexPath).row].url
        self.navigationController?.pushViewController(vc, animated: true)
    }
}
```

以上で、フィードを取得することができました。

| 新着一覧 |
|:-----------:|
|<img width="375" alt="Simulator Screen Shot 2016.09.21 午後1.14.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/60014/5da0480e-c864-d0f5-b728-0e0ae3b25a35.png">|


# まとめと展望

## まとめ

今回は、ReSwiftを使ったReduxライクなアーキテクチャを利用した実装を試みました。

記述量は増えるもののStateを意識して実装を行うことで、新しい実装を行おうとした場合に、新規Actionを今の手順で追加していくだけなので新しく参入したメンバーなどにもプロジェクトのルール説明が行いやすいと思います。

また、Web側もAPIとフロントを切り離して開発しているので
スマホアプリを作る際に、APIを別立てすることなく再利用できる点も魅力だと思います。

## 展望

次回は、SwiftにおけるReactライクな実装である[alexdrone/Render][4]を用いて
JSXのようにViewの操作をまとめて記述したいと思っています。

# おわりに
[サイバー・バズでは一緒にサービスを創れるエンジニアを募集しています :)][5]

[1]:http://qiita.com/yuinchirn/items/286353f5c80cc44f1caf
[2]:https://speakerdeck.com/hakota/reduxworesutorandeli-erutoli-jie-gabu-tutajian
[4]:https://github.com/alexdrone/Render
[5]:https://www.wantedly.com/projects/70360
[6]:http://qiita.com/peromasamune/items/2467035f2fbc04c7f76a
[7]:https://itunes.apple.com/jp/app/qiitenus/id1160686503?mt=8