RxFlow를 도입하며 전 글에서 다룬 개념을 구체화 하기 위해 작성합니다.
작성된 글은 잘못되었을 가능성이 있습니다.
// A FlowCoordinator handles the navigation of a Flow, based on its Stepper and the FlowContributors it emits
public final class FlowCoordinator: NSObject {
private var childFlowCoordinators = [String: FlowCoordinator]()
private weak var parentFlowCoordinator: FlowCoordinator? {
// ...
}
// Rx PublishRelays to handle steps and navigation triggering
private let stepsRelay = PublishRelay<Step>()
fileprivate let willNavigateRelay = PublishRelay<(Flow, Step)>()
fileprivate let didNavigateRelay = PublishRelay<(Flow, Step)>()
// FlowCoordinator unique identifier
internal let identifier = UUID().uuidString
// ...
public func coordinate (flow: Flow, with stepper: Stepper = DefaultStepper(), allowStepWhenDismissed: Bool = false) {
// ...
}
}
FlowCoordinator는 Stepper와 FlowContributors의 emit에 따라
Flow의 Navigation
을 정의한다.
internal steps relay
를 통해 FlowContributor's Step과 Stepper's Step을 받아 Flow에게 전달해준다.
SceneDelegate(or AppDelegate)에서 FlowCoordinator 인스턴스를 가지고 있고, 어떤 Flow가 어떤 Stepper한테 Step을 받을지를 결정한다.
var coordinator: FlowCoordinator = .init()
func scene(...) {
//...
let provider: ServiceProviderType = ServiceProvider()
let appFlow = AppFlow(with: window, and: provider)
let appStepper = AppStepper(provider: provider)
coordinator.coordinate(flow: appFlow, with: appStepper)
}
위 코드에서 FlowCoordinator는 coordinate함수를 통해 AppStepper에게 Step을 받아 AppFlow에게 Step을 넘겨준다.
/// a Stepper has only one purpose is: emits Steps that correspond to specific navigation states.
/// The Step changes lead to navigation actions in the context of a specific Flow
public protocol Stepper {
/// the relay used to emit steps inside this Stepper
var steps: PublishRelay<Step> { get }
/// the initial step that will be emitted when listening to this Stepper
var initialStep: Step { get }
/// function called when stepper is listened by the FlowCoordinator
func readyToEmitSteps ()
}
Stepper는 navigation states와 관련있는 Steps를 emit한다.
Stepper protocol다음과 같이 구성되어있다.
steps: PublishRelay<Step>
initialStep: Step
func readyToEmitSteps()
initialStep이 없다면 NoneStep()을 방출한다.
위 코드에서 FlowCoordinator가 appStepper를 호출하였으므로 initialStep
을 호출 한 뒤, readyToEmitSteps
가 호출된다.
struct AppStepper: Stepper {
// ...
func readyToEmitSteps() {
provider.loginService.didLoginObservable
.map { $0 ? SampleStep.loginIsCompleted : SampleStep.loginIsRequired }
.bind(to: steps)
.disposed(by: disposeBag)
}
}
AppStepper의 readyToEmitSteps
가 호출되며 앱이 로그인되어있는지 아닌지를 검사한다.
그렇게 self의 steps Relay에 로그인 여부에 대한 이벤트를 방출한다.
/// A Flow defines a clear navigation area. Combined to a Step it leads to a navigation action
public protocol Flow: AnyObject, Presentable, Synchronizable {
/// the Presentable on which rely the navigation inside this Flow. This method must always give the same instance
var root: Presentable { get }
/// Adapts an incoming step before the navigate(to:) function
/// - Parameter step: the step emitted by a Stepper within the Flow
/// - Returns: the step (possibly in the future) that should really by interpreted by the navigate(to:) function
func adapt(step: Step) -> Single<Step>
/// Resolves FlowContributors according to the Step, in the context of this very Flow
///
/// - Parameters:
/// - step: the Step emitted by one of the Steppers declared in the Flow
/// - Returns: the FlowContributors matching the Step. These FlowContributors determines the next navigation steps (Presentables to
/// display / Steppers to listen)
func navigate(to step: Step) -> FlowContributors
}
Flow는 clear Navigation Area를 정의한다. Step과 Combine하여 navigation Action을 일으킨다.
var root: Presentable
, func adapt(step: Step)
, func navigate(to step: Step)
를 가지고있다.
root
는 최상단에 존재할 Presentable을 정의한다.
adapt()
는 Stepper에서 전달된 step을 상황에 맞게 다른 step으로 변환할 때 사용하는 것 같다. (사용 해보지 않음)
adapt()
는 정의해주지 않으면 .just(step)을 반환한다.
func navigate(to:)
함수는 Stepper에서 온 step을 받아 어떤 FlowContributors를 반환할 지 결정한다.
먼저 Flow가 준수하는 Protocol부터 살펴보겠다.
Flow는 AnyObject, Presentable, Synchronizable을 준수한다.
AnyObject 는 class 객체, Synchronizable 싱크를 맞춰준다.
/// An abstraction of what can be presented to the screen. For now, UIViewControllers and Flows are Presentable
public protocol Presentable {
/// Rx Observable that triggers a bool indicating if the current Presentable is being displayed
/// (applies to UIViewController, Flow or UIWindow for instance)
var rxVisible: Observable<Bool> { get }
/// Rx Observable (Single trait) triggered when this presentable is dismissed
var rxDismissed: Single<Void> { get }
}
위 코드에서 AppStepper는 FlowCoordinator에 의해 실행되었고, initialStep
을 가지고 있지 않으므로 readyToEmitSteps
에 의해 SampleStep을 방출하게 된다.
AppFlow는 AppStepper에게서 방출된 step을 받아서 func navigate(to:)
함수를 실행시킨다.
func navigate(to step: Step) -> FlowContributors {
guard let step = step as? SampleStep else { return .none }
switch step {
/// 앱을 처음 시작해 로그인이 되어있지 않을 경우 로그인 화면으로 이동
case .loginIsRequired:
return coordinateToLoginVC()
/// mainTabBarIsRequired 호출 시 MainFlow와 nextStep을 넘겨준다.
case .mainTabBarIsRequired, .loginIsCompleted:
return coordinateToMainVC()
default:
return .none
}
}
Presentable프로토콜은 var rxVisible: Observable<Bool>
과 var rxDismissed: Single<Void>
를 가진다.
rxVisible은 current Presentable이 displayed 되고 있는지 여부를 결정해준다.
rxDismissed는 Presentable이 dismiss될 때 trigger된다.
Self: Flow일 경우에 위 두 변수는 self.root의 여부를 판단한다.
앱을 처음 켠 상태이므로 .loginIsRequired
이 불리게 될 것이다.
따라서 사용자가 커스텀한 func coordinateToLoginVC()
함수가 FlowContributors를 반환하게 될 것이다.
private func coordinateToLoginVC() -> FlowContributors {
let loginFlow = LoginFlow(with: provider)
Flows.use(loginFlow, when: .created) { [unowned self] root in
self.rootWindow.rootViewController = root
}
let nextStep = OneStepper(withSingleStep: SampleStep.loginIsRequired)
return .one(flowContributor: .contribute(withNextPresentable: loginFlow,
withNextStepper: nextStep))
}
coordinateToLoginVC
는 LoginFlow를 init해주고, Flows.use
함수를 호출한다.
/// Utility functions to synchronize Flows readyness
public enum Flows {
public enum ExecuteStrategy {
case ready
case created
}
public static func use<Root: UIViewController>(_ flows: [Flow],
when strategy: ExecuteStrategy,
block: @escaping ([Root]) -> Void) {
let roots = flows.compactMap { $0.root as? Root }
guard roots.count == flows.count else {
fatalError("Type mismatch, Flows roots types do not match the types awaited in the block")
}
switch strategy {
case .created:
block(roots)
case .ready:
let flowsReadinesses = flows.map { $0.rxFlowReady }
_ = Single.zip(flowsReadinesses) { _ in Void() }
.asDriver(onErrorJustReturn: Void())
.drive(onNext: { _ in
block(roots)
})
}
}
}
Flows는 Enum타입이다.
내부적으로 .ready
와 .created
case를 가지는 ExecuteStrategy와
static func use<Root: UIViewController>()
를 가진다.
.ready
와 .created
는 단어 그대로 Flow가 사용할 준비가 되었을 때 / Flow가 생성되었을 때 를 나눠준다.
.ready
같은 경우는 내부적으로 숨겨져 있는 flowReadySubject
가 Ready Event를 방출하게 되면 콜백을 돌려준다.
use(flows:)
함수는 [Flow] 를 받아서 callback으로 해당 Flow의 root: Presentable
인 [Root]를 넘겨준다.
Flows.use()
에서 받은 callBack으로 window.rootViewController를 MainFlow.root로 교체해준다.
필자는 LoginStepper를 따로 정의해주지 않았다.
이럴 경우, OneStepper 로 단일 Step을 정의해 줄 수 있다.
따라서 loginFlow는 OneStepper에서 정의된 .loginIsRequired
를 전달받게 된다.
coordinateToLoginVC
가 반환하는 FlowContributors에 대해 알아보자.
public enum FlowContributors {
/// a Flow will trigger several FlowContributor at the same time for the same Step
case multiple (flowContributors: [FlowContributor])
/// a Flow will trigger only one FlowContributor for a Step
case one (flowContributor: FlowContributor)
/// a Flow will trigger a special FlowContributor that represents the dismissal of this Flow
case end (forwardToParentFlowWithStep: Step)
/// no further navigation will be triggered for a Step
case none
/// same as .one(flowContributor: .forwardToParentFlow(withStep: Step)). Should not be used anymore
@available(*, deprecated, message: "You should use .one(flowContributor: .forwardToParentFlow(withStep: Step))")
case triggerParentFlow (withStep: Step)
}
Enum타입으로 .multiple
, .one
, .end
, .none
을 가진다.
.none
은 말 그대로 아무 Step도 방출하지 않는다..end
는 해당 Flow를 dismissal시킨다..one
은 하나의 FlowContributor를, .multiple
은 여러개의 FlowContributor를 방출시킨다.public enum FlowContributor {
/// the given stepper will emit steps, according to lifecycle of the given presentable, that will contribute to the current Flow
/// `allowStepWhenNotPresented` can be passed to make the coordinator accept the steps from the stepper even id
/// the presentable is not visible
/// `allowStepWhenDismissed` can be passed to make the coordinator accept the steps from the stepper even
/// the presentable has dismissed (e.g UIPageViewController's child)
case contribute(withNextPresentable: Presentable,
withNextStepper: Stepper,
allowStepWhenNotPresented: Bool = false,
allowStepWhenDismissed: Bool = false)
/// the "withStep" step will be forwarded to the current flow
case forwardToCurrentFlow(withStep: Step)
/// the "withStep" step will be forwarded to the parent flow
case forwardToParentFlow(withStep: Step)
/// Shortcut static func that returns a .contribute(withNextPresentable: _, withNextStepper: _)
/// in case we have a single actor that is a Presentable and also a Stepper
///
/// - Parameter nextPresentableAndStepper
/// - Returns: .contribute(withNextPresentable: withNext, withNextStepper: withNext)
public static func contribute(withNext nextPresentableAndStepper: Presentable & Stepper) -> FlowContributor {
return .contribute(withNextPresentable: nextPresentableAndStepper, withNextStepper: nextPresentableAndStepper)
}
}
FlowContributor는 단어 Flow에 conribute할 다음 thing을 결정한다.
Enum타입으로 정의되어있고, contribute(...)
, forwardToCurrentFlow(step:)
, forwardToParentFlow(step:)
case 와 static func contribute(...)
를 가진다.
forwardToCurrentFlow(step:)
, forwardToParentFlow(step:)
은 단어 그대로 파라미터로 받은 step을 현재 / 부모 Flow로 넘겨준다.
contribute(...)
case는 다음 Presentable과 다음 Stepper를 파라미터로 받는다.
여기서 받은 Presentable을 표시해주고 Stepper와 Flow를 연결(?) 시켜준다.
allowStepWhenNotPresented
, allowStepWhenDismissed
는 Presentable이 표시되지 않았을 때, dismiss되었을 때 step을 coordinator가 accept할 지를 결정한다.
정리하자면 coordinateToLoginVC()
함수는
1. MainFlow에서 LoginFlow를 child로 가지며
2. root를 바꾸고
3. .loginIsRequired
를 step과 LoginFlow 가진 FlowContributor를 반환한다.
그렇게 반환된 FlowContributor를 FlowCoordinator가 받아서 LoginFlow의 Root를 보여주고, .loginIsRequired
Step을 LoginFlow에 전달한다.
final class LoginFlow: Flow {
var root: Presentable {
return self.rootViewController
}
private let rootViewController: UINavigationController = .init()
// ...
func navigate(to step: Step) -> FlowContributors {
guard let step = step.asSampleStep else { return .none }
switch step {
case .loginIsRequired:
return coordinateToLogin()
// ..
}
}
private func coordinateToLogin() -> FlowContributors {
let reactor = LoginReactor(provider: provider)
let vc = LoginVC(with: reactor)
self.rootViewController.pushViewController(vc, animated: true)
return .one(flowContributor: .contribute(withNextPresentable: vc,
withNextStepper: reactor))
}
}
LoginFlow는 UINavigationController를 root로 가지고있기 때문에 UINavigationController를 먼저 보여준다.
그 후 MainFlow에서 전달받은 step을 navigate(to:)
함수에서 처리해 coordinateToLogin()을 통해 LoginVC를 보여주게 된다.
글 너무 잘 읽었습니다:) FlowCoordinator는 사실상 SceneDelegate에서 한번만 쓰이게 될까요? 최초 근원지가 필요해서 사용한 듯하고, 나머지는 Flow + FlowContrubutor가 비슷한 일을 다 해주는 느낌이어서요~