このFrameworkはつい膨れ上がってしまうViewControllerの肥大化を防ぎ、
かつ乱雑になりがちな画面操作周りのコードを整理し保守性を高めるという目的で作られました。
すでにあるAppleのUIKitといったFrameworkとの親和性も考えて作られてあります。
なお、このFrameworkが主にサポートするのは以下の3点(4点)です。
- 画面の描画ロジックと遷移ロジックの分離
- 画面の描画初期時に必要となるデータの受け渡し
- 画面のライフサイクル管理
- RxSwiftのサポート(optional)
- Xcode 8.1+
- Swift 3.0+
- CocoaPods 1.1.0+ or Carthage 0.18.1+
- デフォルト
- Podfile
# Podfile use_frameworks! target '{YOUR_TARGET_NAME}' do pod 'KabuKit', '0.0.1' pod 'RxSwift', '3.0' pod 'RxCocoa', '3.0' end
- Shellコマンド
$ pod install
- Podfile
- Rx Support なし
- Podfile
# Podfile use_frameworks! target '{YOUR_TARGET_NAME}' do pod 'KabuKit/Scene', '0.0.1' pod 'RxSwift', '3.0' pod 'RxCocoa', '3.0' end
- Shellコマンド
$ pod install
- Podfile
- デフォルト
- Cartfile
github "crexista/KabuKit"
- Shellコマンド
carthage update --platform iOS
- Run Script Build Phase
/usr/local/bin/carthage copy-frameworks
- Input files
$(SRCROOT)/carthage/Build/iOS/KabuKit.framework $(SRCROOT)/carthage/Build/iOS/RxSwift.framework # if you've not added
- Cartfile
以下のシンプルなUIViewControllerを pushViewController/popViewController するだけのアプリ
- SampleAViewcontroller
import UIKit class Sample1AViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBOutlet weak var nextButtonA: UIButton! @IBOutlet weak var nextButtonB: UIButton! @IBOutlet weak var prevButton: UIButton! }
- SampleBViewcontroller
import UIKit class Sample1BViewController: UIViewController { @IBOutlet weak var label: UILabel! @IBOutlet weak var nextButtonA: UIButton! @IBOutlet weak var nextButtonB: UIButton! @IBOutlet weak var prevButton: UIButton! }
extensionでActionScene Protocolを実装します
ex) Sample1AViewController
import Foundation
import KabuKit
extension Sample1AViewController : Scene {
// MARK: - SceneTransition Protocol
enum Sample1Link : SceneTransition {
typealias StageType = UIViewController
case A, B
func request(context: SceneContext<UIViewController>) -> SceneRequest? {
switch self {
case .A:
let xib = ViewControllerXIBFile("Sample1AViewController", Bundle.main)
return context.sceneRequest(xib, Sample1AViewController.self, true) { (stage, scene) in
stage.navigationController?.pushViewController(scene, animated: true)
}
case .B:
let xib = ViewControllerXIBFile("Sample1BViewController", Bundle.main)
return context.sceneRequest(xib, Sample1BViewController.self, nil) { (stage, scene) in
stage.navigationController?.pushViewController(scene, animated: true)
}
}
}
}
// MARK: - ActionScene Protocol
typealias TransitionType = Sample1Link
typealias ArgumentType = Bool
var isRemoval: Bool {
return self.argument!
}
func onRemove(stage: UIViewController) {
_ = stage.navigationController?.popViewController(animated: true)
}
func onPressAButton(sender: UIButton) {
self.director?.changeScene(transition: Sample1Link.A)
}
func onPressBButton(sender: UIButton) {
self.director?.changeScene(transition: Sample1Link.B)
}
func onPressPrevButton(sender: UIButton) {
self.director?.exitScene()
}
// MARK: - Override
override func viewDidLoad() {
prevButton.isEnabled = self.argument!
nextButtonA.addTarget(self, action: #selector(onPressAButton(sender:)), for: .touchUpInside)
nextButtonB.addTarget(self, action: #selector(onPressBButton(sender:)), for: .touchUpInside)
prevButton.addTarget(self, action: #selector(onPressPrevButton(sender:)), for: .touchUpInside)
}
override func viewDidDisappear(_ animated: Bool) {
if (self.navigationController == nil && !isReleased) {
director?.exitScene()
}
}
}
-
Handle SceneTransition
// MARK: - SceneTransition Protocol enum Sample1Link : SceneTransition { typealias StageType = UIViewController case A, B func request(context: SceneContext<UIViewController>) -> SceneChangeRequest? { switch self { case .A: let xib = ViewControllerXIBFile("Sample1AViewController", Bundle.main) return context.sceneRequest(xib, Sample1AViewController.self, true) { (stage, scene) in stage.navigationController?.pushViewController(scene, animated: true) } case .B: let xib = ViewControllerXIBFile("Sample1BViewController", Bundle.main) return context.sceneRequest(xib, Sample1BViewController.self, nil) { (stage, scene) in stage.navigationController?.pushViewController(scene, animated: true) } } } }
上記の
Sample1AViewController
のサンプルではクラス内にenumSample1Link
が定義されており
そしてSample1Link
は SceneTransition の実装となっています。
SceneTranstionの実装である事を宣言した場合以下の1つのメソッドと1つのtypealiasを宣言する必要があります- StageType
このアプリケーション全体を通して表示の規定となるクラスの型です。後述しますがSequence起動時に渡されるクラスの型でもあります。
基本的なiOSアプリケーションにおいてが大体の場合、UIViewControllerになります - request ユーザーからのなんらかのアクションによって画面遷移をする事になった際、どのような時にどのような遷移を行うかのハンドリングを行うメソッドです。
このサンプルでは各クラス(Sample1AViewController, Sample1BViewController)内にenumでそれぞれ定義していますが
共通化させ別ファイルにしてそれぞれのクラスで同じTransitionを使用するというのも可能です。 - StageType
このアプリケーション全体を通して表示の規定となるクラスの型です。後述しますがSequence起動時に渡されるクラスの型でもあります。
-
Implements Scene
// MARK: - ActionScene Protocol typealias TransitionType = Sample1Link typealias ArgumentType = Bool var isRemoval: Bool { return self.argument! } func onRemove(stage: UIViewController) { _ = stage.navigationController?.popViewController(animated: true) }
Scene Protocolの実装である事を宣言した場合、上記のようなコードを書く必要があり以下の1つのメソッドと2つのtypealiasを宣言する必要があり、 そして3つのプロパティが提供されます
-
typealias
ArgumentType
ActionSceneを実装しているクラスが画面を描画する際に必要となる情報の型です。
このサンプルでは戻るボタンを有効にするか否かの情報を送るためにBool型を指定していますTransitionType
ユーザーからのなんらかのアクションによって画面遷移をする事になった際、どのような時にどのような遷移を行うか
というのを記述したクラス(正確にはSceneTransitionを実装したクラス)の型をここに指定します。
今回に場合はクラス内に記述されている実装したEnum、Sample1Link
を指定しています。
-
method
isRemovable
この画面が破棄可能かどうかを返すゲッターです。
このプロパティが常にfalseを返すようにすると、画面を破棄することができなくなります。onRemove
上記のisRemovalがtrueを返した時に呼ばれます。
その際このクラスが指定しているSceneTransitionクラスのStageTypeのクラスが引数としてよばれます
このメソッドが呼ばれるとdirector等、Sceneが持っているプロパティが全て破棄されます
-
-
Properties
func onPressAButton(sender: UIButton) { self.director?.changeScene(transition: Sample1Link.A) } func onPressBButton(sender: UIButton) { self.director?.changeScene(transition: Sample1Link.B) } func onPressPrevButton(sender: UIButton) { self.director?.exitScene() } // MARK: - Override override func viewDidLoad() { prevButton.isEnabled = self.argument! nextButtonA.addTarget(self, action: #selector(onPressAButton(sender:)), for: .touchUpInside) nextButtonB.addTarget(self, action: #selector(onPressBButton(sender:)), for: .touchUpInside) prevButton.addTarget(self, action: #selector(onPressPrevButton(sender:)), for: .touchUpInside) }
上記のコードを見てわかるように、Sceneをimplementsすると
director
とargument
というpropertyが提供されます。 それぞれに責務は以下のとおりです。-
director
Sceneを変更させるメソッドchangeScene
とexitScene
を提供します。
-changeScene
TransitionType で定義されたクラスのインスタンスを引数に取ります。このメソッドを呼ぶとSceneTransitionクラスが呼ばれ画面遷移がされますSwift typealias TransitionType = Sample1Link director.changeScene(Sample1Link.A)
-exitScene
現状の画面から離脱します、が、離脱できない場合何も起きません(後述) -
argument
Sceneを初期化させるのに必要なプロパティです
サンプルの
viewDidLoad
ではActionの初期化がされ且つ、activateが行われてますが、
このフレームワーク的にはどこでActionの初期化を行うかは規定していません。
このサンプルではviewDidLoad
が最適だっただけで、アプリによってはviewWillAppear
で毎回初期化するのがいい場合もあります。
また、このフレームワークにおいてはPresentationロジックはViewController側に書く事はあまり推奨されていません(とはいえ書けますが)。
PresentationロジックはActionに書く事進められています。
Actionの実装の仕方については次項にて説明します。 -
SceneとなるViewControllerの準備ができたらAppDelegateにて呼び出しのコードを書きます
let root = UIViewController()
let xibFile = ViewControllerXIBFile("Sample1AViewController", Bundle.main)
sceneSequence = SceneSequence(root)
// Sequence Start
// stageはroot, sceneはSample1AViewControllerのインスタンス
sceneSequence?.start(xibFile, Sample1AViewController.self, { (stage, scene) in
stage.addChildViewController(scene)
stage.view.addSubview(scene.view)
})
SceneとなるViewControllerをnewで呼び出すことはできません。
呼び出したとしても先述した director
と argument
property はnilのままです。
Sceneの初期化には SceneSequence
を上記コードのように使ってください
前述の方法でSceneをViewControllerにimplementsさせれば基本的に使えますが、このままだとやはり、ViewControllerは肥大化していきます。
そこでRxSwiftを使った ActionScene
というprotocolもこのフレームワークは提供しています
基本的なtypealiasとmethodの実装形式は Scene
の時とは変わりません。
新に observer
というpropertyが一つ追加されるだけです。
この observer
によって以下のようにViewController内部のロジックを複数のActionに分割することが可能になります
// MARK: - Override
override func viewDidLoad() {
self.navigationItem.hidesBackButton = true
let actionA = Sample1AAction(label: label, buttonA: nextButtonA, buttonB: nextButtonB, prevButton: prevButton)
let actionB = Sample1BAction(label: label, buttonA: nextButtonA, buttonB: nextButtonB, prevButton: prevButton)
self.observer.activate(action: actionA, director: self.director, argument: self.argument)
self.observer.activate(action: actionB, director: self.director, argument: self.argument)
}
Action Protoolを以下のように実装します
import Foundation
import KabuKit
import RxSwift
import RxCocoa
class Sample1AAction: Action {
unowned let label: UILabel
unowned let nextButtonA: UIButton
unowned let nextButtonB: UIButton
unowned let prevButton: UIButton
typealias SceneType = Sample1AViewController
func start(director: SceneDirector<Sample1AViewController.Sample1Link>?, argument: Bool?) -> [Observable<()>] {
return [
self.nextButtonA.rx.tap.do(onNext: { () in director?.transitTo(link: Sample1AViewController.Sample1Link.A)}),
self.nextButtonB.rx.tap.do(onNext: { () in director?.transitTo(link: Sample1AViewController.Sample1Link.B)}),
self.prevButton.rx.tap.do(onNext: { () in _ = director?.exit()})
]
}
func onStop() {
// TODO implement
}
func onError(error: Error) {
// TODO implement
}
init(label: UILabel, buttonA: UIButton, buttonB: UIButton, prevButton: UIButton) {
self.label = label
self.nextButtonA = buttonA
self.nextButtonB = buttonB
self.prevButton = prevButton
}
}
- Actionクラスの実装について
Action Protocolを宣言した場合は上記のようなメソッドとtypealiasを指定する必要があります
typealias SceneType = Sample1AViewController func start(director: SceneDirector<Sample1AViewController.Sample1Link>?, argument: Bool?) -> [Observable<()>] { return [ self.nextButtonA.rx.tap.do(onNext: { () in director?.transitTo(link: Sample1AViewController.Sample1Link.A)}), self.nextButtonB.rx.tap.do(onNext: { () in director?.transitTo(link: Sample1AViewController.Sample1Link.B)}), self.prevButton.rx.tap.do(onNext: { () in _ = director?.exit()}) ] } func onStop() { // TODO implement if you need } func onError(error: Error) { // TODO implement if you need }
- SceneType
このActionがどのSceneに紐付いているか定義します。 - start
Actionを起動させます。具体的にはここで返しているRxSwiftのObservableを一括でsubscribeします。 - onStop
このActionを停止した際に呼ばれます。
終了処理ではなく、停止した際、という事に注意してください。 - onError
startでsubscribeされたSigalがなんらかのエラーを起こし、そしてキャッチし損ねた場合、ここに辿りつきます
- SceneType
- 基本概念
ユーザーのインタラクションを受け、描画切り替えを行ったり内部的にAPI通信したりする画面のことをSceneと呼びます。
実装の詳細に関しては上述の Usage を見てください
Sceneを表示するために必要となるUIComponentを示す概念です。
概念そのもののためStageを示すクラスもプロトコルもこのフレームワークには存在しません。
(iOSの実装の場合、往々にして UIViewController
になるのがほとんどです)
ただ、変数名、関数の引数としては存在しており
このフレームワークのプロトコル等を実装した際に Stage
という単語が出てきた場合は
このことを示しています
Sceneの遷移を管理するクラスです。
一番最初に表示すべきSceneを定めるのもこのクラスの責務であるため、
init時にstageとなるオブジェクトを渡す必要があり、start時には最初のSceneを設定する必要があります。
let sequence = SceneSequence(UIViewController())
sequence?.start(xibFile, Sample1AViewController.self, { (stage, scene) in
stage.addChildViewController(scene)
stage.view.addSubview(scene.view)
})
画面を管理するクラスです。
他の画面へ遷移させたい、もしくは現在の画面から離脱したい、といった場合
以下のように changeScene
または exitScene
を呼びます
func onPressAButton(sender: UIButton) {
self.director?.changeScene(transition: Sample1Link.A)
}
func onPressBButton(sender: UIButton) {
self.director?.changeScene(transition: Sample1Link.B)
}
func onPressPrevButton(sender: UIButton) {
self.director?.exitScene()
}
なお、exitSceneが呼ばれ、かつ、Sceneの isRemovable
プロパティが true
を返した場合、
Sceneに紐付いている SceneDirector
と Argument
はメモリ解放されます
画面遷移ロジックを定義したProtocolです。
このSceneTransitionを実装したクラス自体が画面遷移を行うわけではなく、
このSceneTransitionを受け取ったSceneDirectorクラスが画面遷移を行います。
// MARK: - SceneTransition Protocol
enum Sample1Link : SceneTransition {
typealias StageType = UIViewController
case A, B
func request(context: SceneContext<UIViewController>) -> SceneChangeRequest? {
switch self {
case .A:
let xib = ViewControllerXIBFile("Sample1AViewController", Bundle.main)
return context.sceneRequest(xib, Sample1AViewController.self, true) { (stage, scene) in
stage.navigationController?.pushViewController(scene, animated: true)
}
case .B:
let xib = ViewControllerXIBFile("Sample1BViewController", Bundle.main)
return context.sceneRequest(xib, Sample1BViewController.self, nil) { (stage, scene) in
stage.navigationController?.pushViewController(scene, animated: true)
}
}
}
}