RxFlow로 Coordinator 고급스럽게 사용하기

발견한 Coordinator의 문제점

  • Rx를 대부분으로 사용하는 프로젝트에서 Coordinator만 Reactive하지 않다는 문제점
  • 사용자 액션 및 이벤트를 Coordinator에게 전달할 때, 넘어가야할 객체의 수가 많은 경우가 존재
  • 기존에는 Delegate Pattern으로 이벤트를 전달했지만 중복된 코드가 많아지고 코드의 가시성이 현저히 떨어짐
  • 위 사진은 일반적인 경우지만 더 많은 하나의 이벤트가 훨씬 많은 Delegate 메서드를 필요로 하는 경우도 존재

 

RxFlow

  • 코디네이터 패턴에 ReactiveX(rx)를 접목시켜 만든 라이브러리
  • 고전적인 Delegate Pattern을 탈피하고 Flows를 이용하여 네비게이션이 좀 더 명확해짐
  • FlowCoordinator를 기본적으로 제공하여 Flows사이의 네비게이션을 제어 가능
  • 네비게이션 액션들이 Reactive하기 이루어짐 ⭐️
  • RxFlow에 관련한 개념 및 소스코드는 여기서 참조 가능

 

화면 간 Flow 구현

  • 위 사진은 RxFlow를 사용하기 전 각 화면을 도식화하는 작업
  • 공통된 성격을 가지고 있는 ViewController끼리 하나의 Flow로 분리
  • 앱이 실행 될 때에는 AppFlow로 부터 시작하여 다른 Flow들을 관리함 
    • 여기서 Flow는 Coordinator라고 생각해도 무방
  • 예를 들어 AuthFlow는 로그인, 회원가입에 대한 화면들만 담당하여 로직을 처리하는 객체
@MainActor
public final class AppFlow: Flow {

  // MARK: - Properties

  public var root: Presentable { self.rootViewController }

  /// Used only for testFlow.
  private let rootViewController: FavorTabBarController

  // Comment this Initializer.
  public init() {
    self.rootViewController = FavorTabBarController()
  }

  // MARK: - Navigate
  
  public func navigate(to step: Step) -> FlowContributors {
    guard let step = step as? AppStep else { return .none }
    
    switch step {
    case .splashIsRequired:
      return self.navigateToSplash()

    case .splashIsComplete:
      return self.popFromSplash()

    case .dashboardIsRequired:
      return self.popToDashboard()

    case .authIsRequired:
      return self.navigateToAuth()
      
    case .localAuthIsRequired(let location):
      return self.navigateToLocalAuth(request: location)

    case .localAuthIsComplete:
      return self.popToDashboard()

    case .wayBackToRootIsRequired:
      return self.navigateWayBackToRoot()
      
    case .giftManagementIsRequired:
      return self.navigateToGiftManagement()
      
    default:
      return .none
    }
  }
}
  • 최상위에 있는 AppFlow의 구현 모습
  • Flow 객체는 Flow 프로토콜을 채택하고 navigate(to:)메서드를 통해 화면이 전환된다.
  func navigateToAuth() -> FlowContributors {
    let authFlow = AuthFlow()

    Flows.use(authFlow, when: .created) { root in
      self.rootViewController.dismiss(animated: false) {
        root.modalPresentationStyle = .overFullScreen
        self.rootViewController.present(root, animated: false)
      }
    }

    return .one(flowContributor: .contribute(
      withNextPresentable: authFlow,
      withNextStepper: OneStepper(
        withSingleStep: AppStep.authIsRequired
      )
    ))
  }
  • 만약 .authIsRequired의 액션이 들어오면 navigateToAuth()가 호출
  • RxFlow만의 네이밍으로 바뀌었지만 전체적인 개념은 Coordinator와 동일
  • FlowContributors는 AuthFlow를 하위 Flow로 추가하고 AuthFlow에 있는 .authIsRequired를 호출하는 역할
  • Flows.use의 @escaping Closure를 통해 AuthFlow의 rootView를 가지고 화면 전환이 가능
public final class AuthSignInViewReactor: Reactor, Stepper {

    public var steps = PublishRelay<Step>()
    
    public func mutate(action: Action) -> Observable<Mutation> {
	    switch action {
	    case .signInButtonDidTap:
        let email = self.currentState.email
        let password = self.currentState.password
    	  return .concat([
	        self.requestSignIn(email: email, password: password)
    	      .flatMap { _ in
        	    ReminderAlertManager.shared.fetchReminders()
            	self.steps.accept(AppStep.authIsComplete) 🟢
	            return Observable<Mutation>.empty()
    	      }
          ])
      
    case .findPasswordButtonDidTap:
      self.steps.accept(AppStep.findPasswordIsRequired) 🟢
      return .empty()
  }
}
  • ViewModel에 Stepper라는 프로토콜을 채택하면 steps라는 프로퍼티를 구현
  • 이 steps는 현재 보여지고 있는 Flow를 통해 원하는 Step을 넘겨줌
  • 해당 Flow는 원하는 Action을 받아서 로직을 수행할 수 있는 구조

 

Reference