今回は、新規プロダクト開発時に悩み抜いて選定したアーキテクチャについて書きます。
今までは、iOSの基本的な設計思想であるApple’s MVCを使って、iOSアプリを開発していました。

しかし、開発が進むにつれて以下のような問題が発生しました。
- ViewControllerが肥大化する
- 画面遷移が複雑化する
- データの引き渡しがやりづらい
上記の問題を解決するために、今回新たに開発する新規プロダクトでは、Redux(ReSwift) + ApplicationCoordinator を使った、データの同期と遷移の共通化を実装しています。
本記事では、まず概要を説明し、記事の後半で実際のコードをご紹介致します。
Reduxとは
ReSwiftの根本には、Reduxの思想があります。
Reduxは、UIのstate(状態)を管理をするためのフレームワークです。stateでデータフロー管理する実装にFluxが提案されていますが、ReduxはFluxの概念を拡張してより扱いやすく設計されています。
Reduxのデータフローは以下になります。
- Action 「何をする」という情報を持ったオブジェクトです。
- Store アプリケーションの状態(state)を保持している場所です。最大で1つです。
- State アプリケーションでの状態を表します。
- Reducer ActionとStateを使ってStore内のStateを更新します。
Reduxの3原則
Reduxの基本設計は以下の3つの原則に基づいて設計されています。
上記のデータフローはこの原則に則っていることがよく分かると思います。
- Single source of truth
アプリケーション内でStoreは1つのみとし、Stateは単独のオブジェクトとしてStoreに保持される。 - State is read-only
Stateを直接変更することはできず、actionをStoreへdispatchすることでしかStateは変更できない。 - Mutations are written as pure functions
Stateを変更する関数(Reducer)は純粋な関数にする。
ReSwift
今回、ReduxのSwift実装の
を使用して実装しています。
Redux原則を元にデータフローを制御することで、変更の反映漏れやデータの引き回しの不要になります。
ApplicationCoordinator
ライブラリに頼らない遷移管理パターンで、AppDelegate内でUIWindowを継承して初期化することで、遷移関連を統一した1ファイルで管理することが可能です。
ViewController内のDelegateの宣言が前提の為、一時的に渡したい変数等は簡単に引き継ぐことが可能です。
サンプル
今回はサンプルとして以下のような簡単な実装をReSwift + ApplicationCoordinatorで構築したサンプルコードを紹介します。
入力ボタンで入力専用画面へ遷移されて、入力した内容が最初の画面に反映されています。
実行環境
- mac OS Sierra 10.12.6
- Xcode 9.2
- Swift 4
- ReSwift 4.0.0
ReSwiftは、Cocoapodsで導入しています。
pod 'ReSwift'
Storyboard
Storyboardには簡単なUIコンポーネントを配置しています。
Model
最初にReSwiftの登場するState、Action、Reducerを定義します。
状態は、AppStateの中にツリー構造で定義します。Reducerは、MainStateのextensionで宣言することで、名前空間を制御しています。
//
// AppState.swift
// AppDelgateで初期化し、全体のStateを管理する構造体です。
//
import Foundation
import ReSwift
struct AppState: StateType {
var main = MainState()
}
//
// MainState.swift
// 子State
//
import Foundation
import ReSwift
struct MainState: StateType {
var text: String?
}
//
// InputAction.swift
// Actionの構造体です。変更したいStateの値を宣言しています。
//
import Foundation
import ReSwift
struct InputAction: Action {
var text: String
}
//
// AppReducer.swift
// 大本のReducerです。
//
import Foundation
import ReSwift
func AppReducer(action: Action, state: AppState?) -> AppState {
return AppState(
main: MainState.MainReducer(action: action, state: state?.main)
)
}
//
// MainReducer.swift
// 子のReducerで、今回は、InputAction構造体に反応して新しいStateを生成して返す処理をしています。
//
import Foundation
import ReSwift
extension MainState {
static func MainReducer(action: Action, state: MainState?) -> MainState {
let state = state ?? MainState(text: "")
var newState = state
var mainState = state
switch action {
case _ as InputAction:
mainState = MainState(text: (action as! InputAction).text)
default:
break
}
newState = mainState
return newState
}
}
ApplicationCoordinator
本クラスで各ViewController上で定義した、protocolを使用して遷移を管理します。
AppDelgateで初期化する為、UIWindowをイニシャライザに持ちます。
ViewControllerのprotocolは、extensionで定義するのがオススメです。
//
// ApplicationCoordinator.swift
//
import UIKit
final class ApplicationCoordinator {
private let window: UIWindow
init(window: UIWindow) {
self.window = window
}
public func start() {
guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "VC") as? ViewController else {
return
}
vc.transitionDelegate = self
self.window.rootViewController = UINavigationController(rootViewController: vc)
self.window.makeKeyAndVisible()
}
}
extension ApplicationCoordinator: ViewControllerTransitionDelegate {
func push() {
guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "SecondVC") as? SecondViewController else {
return
}
(self.window.rootViewController as? UINavigationController)?.pushViewController(vc, animated: true)
}
}
AppDelegate
Storeの初期化とApplicationCoordinatorの初期化をします。
//
// AppDelegate.swift
//
import UIKit
import ReSwift
// NOTE: アプリで1つのStoreを作成生成する
let mainStore = Store(
reducer: AppReducer,
state: nil
)
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// NOTE: lazyで定義して、始めて使用されるタイミングでApplicationCoordinatorを初期化しています。
lazy var applicationCoordinator: ApplicationCoordinator = {
return ApplicationCoordinator(window: self.window!)
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
self.applicationCoordinator.start()
return true
}
...
Controller
//
// ViewController.swift
//
import UIKit
import ReSwift
protocol ViewControllerTransitionDelegate: class {
func push()
}
final class ViewController: UIViewController {
weak var transitionDelegate: ViewControllerTransitionDelegate?
@IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 監視作成
// NOTE: 特定の状態のみ監視し、同じ値の場合はSkipする
mainStore.subscribe(self) {
$0.select {
$0.main
}.skipRepeats {
$0.text == $1.text
}
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// 監視削除
mainStore.unsubscribe(self)
}
@IBAction func event(_ sender: Any) {
self.transitionDelegate?.push()
}
}
extension ViewController: StoreSubscriber {
typealias StoreSubscriberStateType = MainState
// NOTE: 監視しているの値が変更されてた場合に呼ばれる
func newState(state: MainState) {
self.label.text = state.text ?? ""
}
}
//
// SecondViewController.swift
//
import UIKit
import ReSwift
final class SecondViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
self.textField.text = mainStore.state.main.text
}
}
extension SecondViewController: UITextFieldDelegate {
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextFieldDidEndEditingReason) {
// NOTE: dispatch関数を使用して、Actionを送信する。対応するReduserが検知し、新たなStateが作成され更新されます。
mainStore.dispatch(InputAction(text: textField.text ?? ""))
}
}
まとめ
iOS開発の課題を全て解決しているわけではありませんが、状態変化が起きた際の処理を一箇所にまとめるこで、可読性が改善し、処理が散らばらないのでデバッグしやすいと感じています。
興味のある方は実際に触ってみると面白いと思います。
ReSwiftの表面的な使い方やアーキテクチャ選定のお役に立てれば幸いです。
ここまでお読みいただきまして、ありがとうございました。
次回は社員インタビュー(若手編)を記事にさせていただく予定です。乞うご期待ください!