Published on

🍎 Swift - RxFlow

Authors
  • avatar
    Name
    이창쀀
    Twitter

RxFlow

Favor ν”„λ‘œμ νŠΈλŠ” ReactorKit을 μ‚¬μš©ν•˜μ—¬ MVVM-CνŒ¨ν„΄μ— RxSwiftλ₯Ό 적극적으둜 ν™œμš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

ν•˜μ§€λ§Œ 저희 ν”„λ‘œμ νŠΈμ—μ„œ Reactiveν•˜μ§€ λͺ»ν•œ 뢀뢄이 ν•˜λ‚˜ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

λ°”λ‘œ Coordinator λΆ€λΆ„μΈλ°μš”..

ν•˜μœ„ Coordinatorλ₯Ό μ’…λ£Œν•˜κΈ° μœ„ν•΄μ„œλŠ” μƒμœ„ Coordinator에 μ ‘κ·Όν•΄μ•Όλ§Œ ν•˜λŠ” ν˜„μƒμ„ λ°œκ²¬ν—€μŠ΅λ‹ˆλ‹€.

λ°”λ‘œ μ•„λž˜ μ½”λ“œμ²˜λŸΌμš”..

self.coordinator.parentCoordinator?.finish(childCoordinator: self.coordinator)

ν•œλˆˆμ— 봐도 ꡉμž₯히 깔끔해보이지 μ•ŠμŠ΅λ‹ˆλ‹€. 😡

ν˜„μž¬ 코디넀이터 ➑️ μƒμœ„ 코디넀이터에 접근을 ν•œ λ’€ λ‹€μ‹œ ν˜„μž¬ 코디넀이터λ₯Ό μ’…λ£Œν•˜λŠ” λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•˜λŠ” 것은 ꡉμž₯히 쒋지 μ•Šμ€ 접근이라고 μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€.

λ•Œλ¬Έμ— 두 가지 방법을 κ³ μ•ˆν•΄λ³΄μ•˜λŠ”λ°μš”.

delegate νŒ¨ν„΄μ„ μ‚¬μš©ν•˜μ—¬ ν•˜μœ„ μ½”λ””λ„€μ΄ν„°μ—μ„œ μ§μ ‘μ μœΌλ‘œ μƒμœ„ 코디넀이터에 μ ‘κ·Όν•˜λŠ” 것을 방지

extension AppCoordinator: CoordinatorFinishDelegate {
  func coordinatorDidFinish(childCoordinator: some Coordinator) {
    self.finish(childCoordinator: childCoordinator)
    
    self.navigationController.viewControllers.removeAll()
    
    switch childCoordinator.self {
    case is AuthCoordinator:
      self.showTabBarFlow()
    case is TabBarCoordinator:
      self.showAuthFlow()
    default:
      break
    }
  }
}

overrideλ₯Ό ν™œμš©ν•˜μ—¬ μΆ”κ°€ 둜직 κ΅¬ν˜„ λ°©μ‹μœΌλ‘œ μ ‘κ·Ό

override func finish(childCoordinator: some Coordinator) {
  super.finish(childCoordinator: childCoordinator)
  
  self.navigationController.viewControllers.removeAll()
  
  switch childCoordinator.self {
  case is AuthCoordinator:
    self.showTabBarFlow()
  case is TabBarCoordinator:
    self.showAuthFlow()
  default:
    break
  }
}

사싀 μ™„λ²½ν•˜κ²Œ λ™μΌν•œ λ‘œμ§μ΄μ§€λ§Œ μ ‘κ·Ό λ°©λ²•μ˜ 차이λ₯Ό κ°–κ³  있고, μ–΄λ–€ 방식이 더 λ‚˜μ„κΉŒλ₯Ό κ³ λ―Όν•˜λ˜ 쀑에

delegate νŒ¨ν„΄μ„ μ‚¬μš©ν•˜λŠ” 방식이 β€œμœ„μž„β€ν•œλ‹€λŠ” 둜직 μΈ‘λ©΄μ—μ„œλŠ” μ§κ΄€μ μ΄μ§€λ§Œ μ½”λ“œ κ°€μ‹œμ„± μΈ‘λ©΄μ—μ„œλŠ” λ³΅μž‘ν•˜λ‹€λŠ” 의견으둜 κ·€κ²°λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

delegate νŒ¨ν„΄μ„ μ–΄λ–»κ²Œ ν•˜λ©΄ 더 μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆμ„κΉŒλ₯Ό κ³ λ―Όν•˜λ˜ 쀑에 rxλ₯Ό ν™œμš©ν•˜λ©΄ λ˜μ§€ μ•Šμ„κΉŒλΌλŠ” 생각에 라이브러리λ₯Ό μ°Ύμ•„λ³΄κ²Œ λ˜μ—ˆκ³ , RxFlowλ₯Ό λ°œκ²¬ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€!

❓ RxFlowλž€

곡식 GitHub μ„€λͺ…은 RxFlowλ₯Ό λ‹€μŒκ³Ό 같이 μ„€λͺ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

RxFlowλŠ” Reactive Flowλ₯Ό ν™œμš©ν•˜μ—¬ Coordinator νŒ¨ν„΄μ„ κ΅¬ν˜„ν•œ iOS 앱을 μœ„ν•œ navigation λΌμ΄λΈŒλŸ¬λ¦¬μ΄λ‹€.

μš”μ•½ν•˜λ©΄ β€œμ½”λ””λ„€μ΄ν„° νŒ¨ν„΄μ— rxλ₯Ό μ ‘λͺ©μ‹œμΌ°λ‹€.” μ •λ„κ² λ„€μš”.

RxFlowλŠ” Coordinator νŒ¨ν„΄μ„ Reactiveν™” μ‹œν‚¨ 것이기 λ•Œλ¬Έμ— Coordinator νŒ¨ν„΄μ˜ μž₯단점을 λ¨Όμ € μ„€λͺ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

😊 μž₯점

  • UIViewControllerμ—μ„œ λ„€λΉ„κ²Œμ΄μ…˜ μ½”λ“œλ₯Ό λΆ„λ¦¬ν•œλ‹€.
  • UIViewControllerλ₯Ό λ‹€λ₯Έ ν™”λ©΄ μ „ν™˜ μƒν™©μ—μ„œλ„ μž¬μ‚¬μš©ν•  수 μžˆλ‹€.
  • μ˜μ‘΄μ„± μ£Όμž…μ„ μ‰½κ²Œ 이룰 수 μžˆλ‹€.

😭 단점

  • 코디넀이터 νŒ¨ν„΄μ˜ 기본적인 λ‘œμ§λ“€μ€ 앱을 bootstrapν•  λ•Œλ§ˆλ‹€ μž‘μ„±ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.
  • 코디넀이터 νŒ¨ν„΄ μŠ€νƒμ˜ ꡐλ₯˜ κ³Όμ •μ—μ„œ boilerplate μ½”λ“œκ°€ 많이 λ°œμƒν•  수 μžˆλ‹€.

κ·Έλ ‡λ‹€λ©΄ RxFlowλŠ” μ–΄λ–€ λ°œμ „μ„ μ΄λ£¨μ—ˆλ‹€κ³  μ£Όμž₯ν•˜κ³  μžˆμ„κΉŒμš”?

  • Flowsλ₯Ό ν™œμš©ν•˜μ—¬ λ„€λΉ„κ²Œμ΄μ…˜μ„ μ’€ 더 λͺ…ν™•ν•˜κ²Œ λ°œμ „μ‹œμΌ°λ‹€.
  • FlowCoordinatorλ₯Ό 기본적으둜 μ œκ³΅ν•˜μ—¬ Flows μ‚¬μ΄μ˜ λ„€λΉ„κ²Œμ΄μ…˜μ„ μ œμ–΄ν•  수 μžˆλ‹€.
  • λ„€λΉ„κ²Œμ΄μ…˜ μ•‘μ…˜λ“€μ΄ Reactiveν•˜κ²Œ 이루어진닀.

그리고 μ•„λž˜λŠ” RxFlowλ₯Ό μ΄ν•΄ν•˜κΈ° μœ„ν•΄ μ•Œμ•„λ‘μ–΄μ•Ό ν•  6가지 μš©μ–΄λ“€μž…λ‹ˆλ‹€.

  • Flow: 각각의 FlowλŠ” μ•±μ˜ λ„€λΉ„κ²Œμ΄μ…˜ 곡간듀을 μ •μ˜ν•©λ‹ˆλ‹€. 이 곡간은 λ„€λΉ„κ²Œμ΄μ…˜ μ•‘μ…˜λ“€μ΄ μ„ μ–Έλ˜λŠ” κ³³μž…λ‹ˆλ‹€.
  • Step: Step은 λ„€λΉ„κ²Œμ΄μ…˜κΉŒμ§€ μ΄μ–΄μ§ˆ 수 μžˆλŠ” stateλ₯Ό ν‘œν˜„ν•˜κΈ° μœ„ν•œ λ°©λ²•μž…λ‹ˆλ‹€. Flow와 Step을 μ‘°ν•©ν•˜λ©΄ κ°€λŠ₯ν•œ λͺ¨λ“  λ„€λΉ„κ²Œμ΄μ…˜ μ•‘μ…˜μ„ μ„€λͺ…ν•  수 μžˆμŠ΅λ‹ˆλ‹€. Step은 (idλ‚˜ URL 같은) 내뢀적인 값듀을 μ§€λ‹ˆκ³  μžˆμ„ μˆ˜λ„ μžˆμ–΄ 이 값듀을 Flow에 μ„ μ–Έλ˜μ–΄ μžˆλŠ” 화면듀에 전달할 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
  • Stepper: Flow μ•ˆμ—μ„œ Step을 λ°œμƒμ‹œν‚¬ 수 μžˆλ‹€λ©΄, κ·Έ μ–΄λ–€ 것도 Stepper라고 뢈릴 수 μžˆμŠ΅λ‹ˆλ‹€.
  • Presentable: Present 될 수 μžˆλŠ” μ–΄λ–€ 것을 μΆ”μƒμ μœΌλ‘œ ν‘œν˜„ν•œ κ°œλ…μž…λ‹ˆλ‹€. (기본적으둜 UIViewController와 Flowκ°€ Presentableν•œ κ°μ²΄μž…λ‹ˆλ‹€.)
  • FlowContributor: μ–΄λ–€ 것듀이 Flow μ•ˆμ—μ„œ μƒˆλ‘œμš΄ Step을 λ§Œλ“€μ–΄λ‚Ό 수 μžˆμ„μ§€λ₯Ό FlowCoordinatorμ—κ²Œ μ•Œλ €μ£ΌλŠ” κ°„λ‹¨ν•œ 데이터 κ΅¬μ‘°μž…λ‹ˆλ‹€.
  • FlowCoordinator: κ°œλ°œμžκ°€ μ μ ˆν•œ Flow와 Step을 μ‘°ν•©ν•˜μ—¬ κ°€λŠ₯ν•œ λ„€λΉ„κ²Œμ΄μ…˜μ„ μ •μ˜ν•˜λ©΄, FlowCoordinatorλŠ” μ•±μ˜ λͺ¨λ“  λ„€λΉ„κ²Œμ΄μ…˜μ„ μ œμ–΄ν•˜κΈ° μœ„ν•΄ 이 쑰합듀을 μ„žμŠ΅λ‹ˆλ‹€. FlowCoordinatorλŠ” RxFlow에 μ˜ν•΄ μ œκ³΅λ˜μ–΄ κ°œλ°œμžκ°€ 직접 κ΅¬ν˜„ν•˜μ§€ μ•Šμ•„λ„ λ©λ‹ˆλ‹€.

μ‚¬μš© μ˜ˆμ‹œ

곡식 λ¬Έμ„œλŠ” μœ„μ™€ 같은 μ˜ν™” 정보앱을 μ˜ˆμ‹œλ‘œ λ“€μ–΄ μ„€λͺ…ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

ν•˜λ‚˜μ”© μ‚΄νŽ΄λ³Όκ²Œμš”..! πŸ˜‰

Step μ •μ˜

μš°μ„  Step을 μ •μ˜ν•΄μ£Όμ–΄μ•Ό ν•©λ‹ˆλ‹€.

ν•œ 가지 μ£Όμ˜ν•  점은 Step은 μ•±μ˜ μƒνƒœλ₯Ό λ‚˜νƒ€λ‚΄λŠ” μš”μ†Œμ΄κΈ° λ•Œλ¬Έμ— μ–΄λ””λ‘œ navigation이 μ΄λ£¨μ–΄μ§ˆ 지와 같은 νŠΉμ •μ μΈ μš”μ†Œλ“€μ€ Step이 μ•„λ‹ˆλΌ Flowμ—μ„œ μ •ν•΄μ€˜μ•Ό ν•œλ‹€κ³  ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄ showMovieDetail(withID: Int)λŠ” μ˜ν™”λ₯Ό μ„ νƒν–ˆμ„ λ•Œ μ˜ν™”μ˜ μ„ΈλΆ€ 정보λ₯Ό λ³΄μ—¬μ£ΌλŠ” 맀우 νŠΉμ •λœ μΌ€μ΄μŠ€μ΄κΈ° λ•Œλ¬Έμ— λ°”λžŒμ§ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

λŒ€μ‹  movieIsPicked(withID: Int)와 같이 쑰금 더 독립적인 μΌ€μ΄μŠ€λ₯Ό μΆ”κ°€ν•΄μ€˜μ•Ό ν•©λ‹ˆλ‹€. 이런 μ‹μœΌλ‘œ μ •μ˜ν•΄μ£Όλ©΄ μ˜ν™”κ°€ 선택됐을 λ•Œ μ˜ν™”μ˜ μ„ΈλΆ€ 정보 화면을 λΆˆλŸ¬μ˜€λŠ” 것 말고도 λ‹€λ₯Έ μ‘°κ±΄μ—μ„œ λ‹€λ₯Έ μ•‘μ…˜μ„ 좔가해쀄 수 μžˆμŠ΅λ‹ˆλ‹€.

μ•„λž˜μ™€ 같이 enum νƒ€μž…μœΌλ‘œ μ •μ˜ν•˜λŠ” 것을 μΆ”μ²œν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

enum DemoStep: Step {
	// Login
	case loginIsRequired
	case userIsLoggedIn

	// Onboarding
	case onboardingIsRequired
	case onboardingIsComplete

	// Home
	case dashboardIsRequired

	// Movies
	case moviesAreRequired
	case movieIsPicked(withID: Int)
	case castIsPicked(withID: Int)

	// Settings
	case settingsAreRequired
	case settingsAreComplete
}

Flow μ •μ˜

Flow μ •μ˜λŠ” 두 가지λ₯Ό ν•„μˆ˜μ μœΌλ‘œ ν•΄μ€˜μ•Όν•©λ‹ˆλ‹€.

  1. λ„€λΉ„κ²Œμ΄μ…˜μ˜ 근간이 λ˜λŠ” root Presentable을 μ„ μ–Έν•΄μ€λ‹ˆλ‹€.
  2. navigate(to:) λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„ν•¨μœΌλ‘œμ„œ Step을 λ„€λΉ„κ²Œμ΄μ…˜ μ•‘μ…˜μœΌλ‘œ λ³€ν™˜ν•΄μ£ΌλŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.

navigate(to:) λ©”μ„œλ“œλŠ” FlowContributorsλ₯Ό λ¦¬ν„΄ν•©λ‹ˆλ‹€.

.one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel)) 의 ν˜•νƒœλ‘œ 리턴을 ν•˜λ©΄ λ˜λŠ”λ°μš”.. 각 νŒŒλΌλ―Έν„°λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • viewController: λͺ¨λ‘κ°€ μ•„λŠ”λ°”μ™€ 같이 Presentable이고, LifeCycle에 따라 μ—°κ²°λœ Stepper(ViewModel)κ°€ Step을 emitν•˜λŠ” 것에 영ν–₯을 μ€λ‹ˆλ‹€.
    • 예λ₯Ό λ“€μ–΄ Presentable이 hidden(presentλ˜μ§€ μ•Šμ€ 상황)일 경우, Stepperκ°€ Step을 emitν•œλ‹€κ³  해도 ν•΄λ‹Ή emit은 효λ ₯이 μ—†μŠ΅λ‹ˆλ‹€.
  • viewModel: Stepperλ‘œμ„œ μ—°κ²°λœ Presentable의 LifeCycle에 영ν–₯을 λ°›μœΌλ©°, Step을 emitν•¨μœΌλ‘œμ„œ Flowμ•ˆμ—μ„œ λ„€λΉ„κ²Œμ΄μ…˜μ— κΈ°μ—¬ν•©λ‹ˆλ‹€.

λΆ€κ°€μ μœΌλ‘œ FlowλŠ” View Controllerλ₯Ό instantiate 해쀄 λ•Œ μ΄λ£¨μ–΄μ§€λŠ” μ˜μ‘΄μ„± μ£Όμž…μ— 이용될 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

class WatchedFlow: Flow {
	var root: Presentable {
		return self.rootViewController
	}

	private let rootViewController = UINavigationController()
	private let services: AppServices

	init(withServices services: AppServices) {
		self.services = services
	}

	func navigate(to step: Step) -> FlowContributors {
		guard let step = step as? DemoStep else { return .none }

		switch step {
		case .moviesAreRequired:
			return self.navigateToMovieListScreen()
		case .movieIsPicked(let movieID):
			return self.navigateToMovieDetailScreen(with: movieID)
		case .castIsPicked(let castID):
			return self.navigateToCastDetailScreen(with: castID)
		default:
			return .none
		}
	}
}

private extension WatchedFlow {

	func navigateToMovieListScreen() -> FlowContributors {
		let viewController = WatchedViewController.instantiate(withViewModel: WatchedViewModel(), andServices: self.services)
		viewController.title = "Watched"

		self.rootViewController.pushViewController(viewController, animated: true)
		return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel))
	}

	func navigateToMovieDetailScreen(with movieID: Int) -> FlowContributors {
		// ...
	}

	func navigateToCastDetailScreen(with castID: Int) -> FlowContributors {
		// ...
	}
}

Stepper μ •μ˜

StepperλŠ” protocol이기 λ•Œλ¬Έμ— μ–΄λ–€ 것이든 될 수 μžˆλ‹€κ³  ν•©λ‹ˆλ‹€. ViewControllerκ°€ 될 μˆ˜λ„ 있고, ViewModel이 될 μˆ˜λ„ 있죠.

ν•˜μ§€λ§Œ ViewModel 같이 λ‘œμ§μ„ 뢄리할 수 μžˆλŠ” 곳에 μ‚¬μš©ν•˜λŠ” 것이 μ μ ˆν•©λ‹ˆλ‹€.

RxFlowλŠ” OneStepperλΌλŠ” Stepper 클래슀λ₯Ό 기본으둜 κ΅¬ν˜„ν•˜μ—¬ μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜μ™€ 같이 μƒκ²ΌλŠ”λ°μš”..

public class OneStepper: Stepper {
	public let steps = PublishRelay<Step>()
	private let singleStep: Step

	public init(withSingleStep singleStep: Step) {
		self.singleStep = singleStep
	}

	public var initialStep: Step {
		return self.singleStep
	}
}

이 OneStepperλŠ” μ΄ˆκΈ°ν™”μ™€ λ™μ‹œμ— 였직 ν•˜λ‚˜μ˜ Step을 emitν•˜λŠ” 것을 μœ μΌν•œ 둜직으둜 κ°–λŠ” Stepperμž…λ‹ˆλ‹€.

μƒˆλ‘œμš΄ Flowλ₯Ό μƒμ„±ν•˜κ³  첫 Step을 μ‹€ν–‰ν•  λ•Œ μœ μš©ν•˜λ‹€κ³  ν•©λ‹ˆλ‹€.

μ•„λž˜ StepperλŠ” pick(movieID:) λ©”μ„œλ“œκ°€ 싀행될 λ•Œλ§ˆλ‹€ DemoStep.movieIsPicked(withID)λ₯Ό emitν•©λ‹ˆλ‹€.

ν•΄λ‹Ή Step이 emit되면, μœ„μ˜ WatchedFlowμ—μ„œ navigate(to step:) λ©”μ„œλ“œκ°€ μ‹€ν–‰λ˜κ³ , 결과적으둜 navigateToMovieDetailScreen(withmovieID: Int) λ©”μ„œλ“œκ°€ μ‹€ν–‰λ˜κ²Œ λ©λ‹ˆλ‹€.

class WatchedViewModel: Stepper {
	let movies: [MovieViewModel]
	let steps = PublishRelay<Step>()

	init(with service: MovieService) {
		self.movies = service.watchedMovies().map({ movie -> MovieViewModel in
			return MovieViewModel(id: movie.id, title: movie.title, image: movie.image)
		})
	}

	// μ˜ν™”κ°€ pick 됐을 λ•Œ μƒˆλ‘œμš΄ Step을 emitν•©λ‹ˆλ‹€.
	// 이 emit은 WatchedFlowμ—μ„œ λ„€λΉ„κ²Œμ΄μ…˜ μ•‘μ…˜μ„ μ΄‰λ°œν•©λ‹ˆλ‹€.
	public func pick(movieID: Int) {
		self.steps.accept(DemoStep.movieIsPicked(withID: movieID))
	}
}