- Published on
π Swift - RxFlow
- Authors
- Name
- μ΄μ°½μ€
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
μ μλ λ κ°μ§λ₯Ό νμμ μΌλ‘ ν΄μ€μΌν©λλ€.
- λ€λΉκ²μ΄μ
μ κ·Όκ°μ΄ λλ root
Presentable
μ μ μΈν΄μ€λλ€. 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))
}
}