Design Patterns

Design Pattern, RxSwift ) RxFlow 맛보기 - 1

JoonSwift 2022. 7. 19. 15:37

RxFlow?

RxFlow는 Reactive Flow Coordinator pattern을 기반으로 한 iOS 앱의 navigation framework입니다.

Why use RxFlow?

  • Storyboard를 더 작은 단위로 잘라내어 UIViewController들의 reusability와 collaboration이 가능하도록 해줍니다.
  • navigation context에 따라 UIViewController가 나타나는 방식을 다양하게 가져갈 수 있습니다.
  • 의존성 주입(Dependency Injection)을 편리하게 구현할 수 있습니다.
  • UIViewController에서 모든 navigation과 관련된 코드를 없앨 수 있습니다.
  • navigation 코드도 Reactive Programming이 가능하게 도와줍니다.
  • 주된 navigation case들을 addressing하면서 선언형 방식으로 navigation을 표현할 수 있습니다.
  • 앱을 논리적인 navigation 블록들로 쉽게 잘라낼 수 있습니다.

Key concept

  • Flow : 각 Flow는 navigation action을 정의하는 부분입니다. ex ) UIViewController를 push 하거나 present하는 코드.
  • Step : Step은 navigation의 상태를 정의하는 곳입니다. Flow와 Step을 조합해서 모든 navigation action을 표현할 수 있습니다.
  • Stepper : Flow 내에 있는 Step을 생성(emit)할 수 있는 모든것입니다. (ViewModel이나 UIViewController)
  • Presentable : presented될 수 있는 것을 추상화한 것입니다. (기본적으로 UIViewController와 Flow가 Presentable입니다.)
  • FlowContributor : FlowCoordinator에게 Flow 내의 Step을 생성(emit)할 다음것이 무엇인지 알려줍니다.
  • FlowCoordinator : 개발자가 Flow와 Step을 잘 조합해서 가능한 모든 navigation 경우의 수를 나타내었다면, FlowCoordinator은 이것들을 잘 종합하여 앱의 navigation을 핸들링 해줍니다.

Project

직접 만들어보면서 감을 잡아보도록 하겠습니다ㅎㅎ

이런 구조를 가진 앱을 만들어보도록 하겠습니다.

Step 구현하기

우선 현재 구현할 앱에서 가능한 navigation 상태들을 담아둘 Step을 만들어보겠습니다.

enum ExampleStep: Step {
  case launchIsRequired
  
  case mainIsRequired
  
  case homeIsRequired
  case mypageIsRequired
  
  case homeNext
  case mypageNext
}

각각의 상태를 나타내보았습니다. 위에 그림에서도 보이듯, 6개의 화면을 6개의 case들로 나타내었습니다.

Flow들 구현하기

제일 처음 앱이 실행되면, Launch Screen을 보여줄지, Main Screen을 그대로 보여줄지 결정하는 Flow를 하나 생성합니다. 이름은 AppFlow라고 하겠습니다.

final class AppFlow: Flow {
  private let window: UIWindow
  
  var root: Presentable {
    return self.window
  }
  
  init(window: UIWindow) {
    self.window = window
  }
  
  func navigate(to step: Step) -> FlowContributors {
    guard let step = step as? ExampleStep else { return .none }
    switch step {
    case .launchIsRequired:
      return navigateToLaunchScreen()
    case .mainIsRequired:
      return navigateToMainTabBar()
    default:
      return .none
    }
  }
  
  private func navigateToLaunchScreen() -> FlowContributors {
    let viewController = LaunchViewController()
    window.rootViewController = viewController
    return .one(flowContributor: .contribute(withNext: viewController))
  }
  
  private func navigateToMainTabBar() -> FlowContributors {
    // ...
  }
}

AppFlow의 root는 UIWindow입니다. 앱이 실행되고, window의 rootViewController를 바꿔주며 보여줄것이기 때문에 root를 UIWindow로 정했습니다.

또한 AppFlow에서는 2가지의 Step에 따라 화면 전환이 일어납니다. launchIsRequired, mainIsRequired.

Launch Screen은 다른 Flow가 필요없이 LaunchViewController만 보여주고 끝날것이라. navigateToLaunchScreen 메서드를 위와같이 구현합니다.

LaunchViewController

final class LaunchViewController: UIViewController, Stepper {
  var steps = PublishRelay<Step>()
  
  //viewDidLoad ... 

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: { [unowned self] in
      self.steps.accept(ExampleStep.mainIsRequired)
    })
  }
}

LaunchViewController는 Stepper입니다. 왜냐하면 아래의 viewDidAppear 메서드에서 특정 시간이 지난 후에, mainIsRequired 라는 Step을 emit할 것이기 때문에 Stepper입니다.

저 코드가 실행되고 나면 다시 AppFlow의 navigate(to : ) 메서드에서 받아온 Step을 확인한 후 mainIsRequired라는 상태에 따라 코드를 실행하게 됩니다.

AppFlow - navigateToMainTabBar()

private func navigateToMainTabBar() -> FlowContributors {
    let flow = MainFlow()
    let stepper = MainStepper()
    
    Flows.use(flow, when: .created, block: { [unowned self] root in
      self.window.rootViewController = root
    })
    
    return .one(flowContributor: .contribute(
      withNextPresentable: flow,
      withNextStepper: stepper
    ))
  }

MainFlow의 경우에는 이후에 또 다른 Navagation 로직이 존재하기때문에 MainFlow를 구현하고, MainStepper 또한 구현해놓은 모습입니다.

Flows.use 메서드를 통해 넘겨준 flow(MainFlow)가 created되는 시점에 실행될 block (여기서는 window의 rootViewController를 해당 flow(MainFlow)의 root로 바꿔주는 코드를 작성해주었습니다.

마지막으로 FlowContributor를 반환해주면서, 다음에 시작될 Flow와 그와 관련된 Stepper를 넘겨주는 코드를 확인할 수 있습니다.

MainFlow

MainFlow에서는 주로 UITabBarController의 navigation 코드를 다룹니다.

final class MainFlow: Flow {
  var root: Presentable {
    return self.tabBarController
  }
  
  private let tabBarController = UITabBarController()
  
  func navigate(to step: Step) -> FlowContributors {
    guard let step = step as? ExampleStep else { return .none }
    switch step {
    case .homeIsRequired:
      return navigateToHome()
    default:
      return .none
    }
  }
  
  private func navigateToHome() -> FlowContributors {
    let homeFlow = HomeFlow()
    let mypageFlow = MyPageFlow()
    
    let homeStepper = HomeStepper()
    let mypageStepper = MyPageStepper()
    
    Flows.use(
      homeFlow, mypageFlow,
      when: .created,
      block: { [unowned self] (root1: UINavigationController, root2: UINavigationController) in

        //Setup TabBar item and title...

        self.tabBarController.setViewControllers([root1, root2], animated: false)
      })
    return .multiple(flowContributors: [
      .contribute(
        withNextPresentable: homeFlow,
        withNextStepper: homeStepper
      ),
      .contribute(
        withNextPresentable: mypageFlow,
        withNextStepper: mypageStepper
      )
    ])
  }
}

class MainStepper: Stepper {
  var steps = PublishRelay<Step>()
  
  var initialStep: Step {
    return ExampleStep.homeIsRequired
  }
}

여기서 제가 생각하는 중요한 부분은 MainStepper의 initialStep을 homeIsRequired로 준 점과, HomeFlow, HomeStepper, MyPageFlow, MyPageStepper를 생성하여 FlowContributor를 multimple로 넘겨준 부분이 중요하게 봐야 할 부분이라고 생각합니다.

HomeFlow, MyPageFlow

final class HomeFlow: Flow {
  var root: Presentable {
    return self.navigationController
  }
  
  private let navigationController = UINavigationController()
  
  func navigate(to step: Step) -> FlowContributors {
    guard let step = step as? ExampleStep else { return .none }
    
    switch step {
    case .homeIsRequired:
      return navigateToHome()
    case .homeNext:
      return navigateToNext()
    default:
      return .none
    }
  }
  
  private func navigateToHome() -> FlowContributors {
    let viewController = HomeViewController()
    viewController.title = "Home"
    navigationController.pushViewController(
      viewController, animated: false
    )
    return .one(flowContributor: .contribute(withNext: viewController))
  }
  
  private func navigateToNext() -> FlowContributors {
    // ...
  }
}
final class MyPageFlow: Flow {
  var root: Presentable {
    return self.navigationController
  }
  
  private let navigationController = UINavigationController()
  
  func navigate(to step: Step) -> FlowContributors {
    guard let step = step as? ExampleStep else { return .none }
    switch step {
    case .mypageIsRequired:
      return navigateToMyPage()
    case .mypageNext:
      return navigateToNext()
    default:
      return .none
    }
  }
  
  private func navigateToMyPage() -> FlowContributors {
    let viewController = MyPageViewController()
    viewController.title = "My Page"
    navigationController.pushViewController(
      viewController, animated: false
    )
    return .one(flowContributor: .contribute(withNext: viewController))
  }
  
  private func navigateToNext() -> FlowContributors {
    // ...
  }
}

HomeFlow, MyPageFlow 둘 다 root를 UINavigationController로 사용하고 있습니다.

또한 HomeStepper, MyPageStepper 에서

class HomeStepper: Stepper {
  var steps = PublishRelay<Step>()
  
  var initialStep: Step {
    return ExampleStep.homeIsRequired
  }
}

class MyPageStepper: Stepper {
  var steps = PublishRelay<Step>()
  
  var initialStep: Step {
    return ExampleStep.mypageIsRequired
  }
}

이렇게 initialStep을 통해 각각의 navigate(to:) 메서드에 있는 알맞는 Step에 대한 메서드를 호출하게 구현했습니다.

NextViewController

Home과 MyPage에서 버튼을 누르면 각각 HomeNextViewController, MyPageNextViewController로 이동할 수 있도록 설계했습니다.

이 과정에서 중요하게 봐야할 점은, HomeViewController, MyPageViewController에 있는 UIButton이 바로 Next로 navigate하는 Step을 제공한다는 것입니다. 즉, Step을 emit하는 HomeViewController, MyPageViewController 둘 다 Stepper가 되어야합니다.

final class HomeViewController: UIViewController, Stepper {
  var steps = PublishRelay<Step>()
	// ...
	@objc func didTappedButton() {
	  self.steps.accept(ExampleStep.homeNext)
	}
}
final class MyPageViewController: UIViewController, Stepper {
  var steps = PublishRelay<Step>()
	// ... 
	@objc func didTappedButton() {
    self.steps.accept(ExampleStep.mypageNext)
  }
}

그러면 HomeFlow와 MyPageFlow의 navigateToNext() 메서드에는 각각

private func navigateToNext() -> FlowContributors {
  let viewController = HomeNextViewController()
  viewController.title = "Home_Next"
  navigationController.pushViewController(
    viewController, animated: true
  )
  return .none
}

private func navigateToNext() -> FlowContributors {
  let viewController = MyPageNextViewController()
  navigationController.present(
    viewController, animated: true
  )
  return .none
}

ViewController를 생성하여 navigation action을 작성해주면 됩니다.

 

구조를 그림으로 그려보았습니다 ㅎㅎ

 

정리

ViewController에 어떤 navigation action이 발생할 때, 거기서 ViewController를 생성하고, 어떻게 보여줄지 결정하는 로직을 작성했었는데, RxFlow, Coordinator pattern을 사용하니 ViewController에서 그러한 로직들을 분리할 수 있어서 보기 좋았던것 같습니다. ㅎㅎ 

하지만 아직 RxFlow를 사용은 해봤는데 정확하게 내부에서 어떻게 동작하는지, 그리고 RxSwift에 대한 이해가 아직 부족해서 사용법을 알아내는데 오래 걸렸었습니다. 

 

'Design Patterns' 카테고리의 다른 글

Iterator Pattern?!  (0) 2022.02.04
느낌대로 만들어본 TabBar, Navigation Coordinator  (0) 2022.01.27
Coordinator Pattern 연습 1  (0) 2022.01.25
Observer Pattern  (0) 2021.06.03