RxSwift)RxFlow 톺아보기

Havi·2021년 4월 14일
1

RxFlow

목록 보기
3/3

RxFlow를 도입하며 전 글에서 다룬 개념을 구체화 하기 위해 작성합니다.
작성된 글은 잘못되었을 가능성이 있습니다.

전체 깃헙 예제 코드

1. FlowCoordinator

FlowCoordinator 구현부

// 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을 넘겨준다.

2. Stepper

Stepper 구현부

/// 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다음과 같이 구성되어있다.

  1. event를 받아 방출할 수 있는 steps: PublishRelay<Step>
  2. 해당 Stepper를 listening할 때 처음 방출하는 initialStep: Step
  3. FlowCoordinator에게 listened되었을 때 실행되는 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에 로그인 여부에 대한 이벤트를 방출한다.

3. Flow

Flow 구현부

/// 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 싱크를 맞춰준다.

Presentable

Presentable 구현부

/// 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함수를 호출한다.

Flows

Flows 구현부

/// 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에 대해 알아보자.

FlowContributors

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을 가진다.

  1. .none은 말 그대로 아무 Step도 방출하지 않는다.
  2. .end는 해당 Flow를 dismissal시킨다.
  3. .one은 하나의 FlowContributor를, .multiple은 여러개의 FlowContributor를 방출시킨다.

FlowContributor

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를 보여주게 된다.

profile
iOS Developer

1개의 댓글

comment-user-thumbnail
2021년 6월 16일

글 너무 잘 읽었습니다:) FlowCoordinator는 사실상 SceneDelegate에서 한번만 쓰이게 될까요? 최초 근원지가 필요해서 사용한 듯하고, 나머지는 Flow + FlowContrubutor가 비슷한 일을 다 해주는 느낌이어서요~

답글 달기