RxFlow란
- Reactive Flow Coordinator패턴을 기반으로 하는 iOS 네비게이션 프레임워크
- Coordinator패턴을 RxSwift에 녹여낸 라이브러리
RxFlow의 장점
- 스토리보드를 unit단위로 분리하여 UIViewController의 재사용 가능
- 네비게이션 흐름에 따라 다양한 방식으로 UIViewController를 나타낼 수 있음
- DI(의존성 주입)을 쉽게 구현 가능
- UIViewController로부터의 모든 네비게이션 메커니즘 제거
- 반응형 프로그래밍 사용 증대
- 모든 네비게이션 케이스의 주요한 부분을 선언적으로 표현 가능
- 어플리케이션을 논리적인 블록으로 분리
Coordinator패턴과의 차이
Coordinator패턴의 특징
- UIViewController부터의 네비게이션 코드 제거
- 다른 네비게이션 컨텍스트에서 UIViewController 재사용 가능
- DI를 쉽게 사용 가능
Coordinator패턴의 단점
- 애플리케이션을 bootstrap(일련의 과정)할 때마다 작성해줘야 한다
- Coordinator 스택과 통신할 때 많은 boiler plate코드 발생 가능
RxFlow의 6가지 특징
- Flow: 각각의 Flow는 애플리케이션의 네비게이션 영역을 정의. 네비게이션 작업(UIViewController또는 다른 Flow표시)를 선언
- Step: Step은 네비게이션 상태를 표현. Flow와 Step의 조합으로 모든 네비게이션 작업을 설명 가능. Step은 Flow화면에서 선언된 화면으로 전파될 내부값(Id, URL..)을 포함 할 수 있다.
- Stepper: Stepper는 Flow내에서 Step을 방출할 수 있는 모든 것. Flow의 모든 네비게이션액션을 emit
- Presentable: 표시될 수 있는 추상화타입(기본적으로 UIViewController 및 Flow는 표시 가능)
- FlowContributor: FlowCoordinator에게 Flow의 새 단계를 생성할 수 있는 다음 항목이 무엇인지 알려주는 데이터 구조
- FlowCoordinator: Presentable과 Stepper를 조합하는 간단한 데이터 구조. FlowCoordinator작업은 이러한 조합을 사용하여 앱의 모든 네비게이션을 처리(FlowCoordinator는 RxFlow에서 제공하므로 구현할 필요x)
RxFlow데모 앱 살펴보기
1. Step정의
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
}
Step은 네비게이션 의도를 표현하는 각각의 상태이며 enum형태로 선언하는것이 편리
각각의 상태에 따른 case정의(Login, Onboarding, Home, Movies, Settings...)
2. Flow정의
Flow는 ViewController를 인스턴스화 할 때 의존성 주입(DI)을 구현하는데 사용
- 네비게이션의 기초가될 root Presentable을 정의
- Step을 네비게이션 액션으로 변환할 navigate(to:) 함수 구현
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 navigateToMovieListScreen()
case .movieIsPicked(let movieId):
return navigateToMovieDetailScreen(with: movieId)
case .castIsPicked(let castId):
return navigateToCastDetailScreen(with: castId)
default:
return .none
}
}
private 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))
}
private func navigateToMovieDetailScreen (with movieId: Int) -> FlowContributors {
let viewController = MovieDetailViewController.instantiate(withViewModel: MovieDetailViewModel(withMovieId: movieId),
andServices: self.services)
viewController.title = viewController.viewModel.title
self.rootViewController.pushViewController(viewController, animated: true)
return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController.viewModel))
}
private func navigateToCastDetailScreen (with castId: Int) -> FlowContributors {
let viewController = CastDetailViewController.instantiate(withViewModel: CastDetailViewModel(withCastId: castId),
andServices: self.services)
viewController.title = viewController.viewModel.name
self.rootViewController.pushViewController(viewController, animated: true)
return .none
}
}
3. Stepper정의
Step을 생성할 수 있는 모든것, Stpper는 Flow내 모든 네비게이션 액션을 트리거
(ViewController에서 프로토콜로 채택할 수 있지만 보통 ViewModel에서 분리시키는 것이 좋다.)
class WatchedViewModel: Stepper {
let movies: [MovieViewModel]
let steps = PublishRelay<Step>()
init(with service: MoviesService) {
// we can do some data refactoring in order to display things exactly the way we want (this is the aim of a ViewModel)
self.movies = service.watchedMovies().map({ (movie) -> MovieViewModel in
return MovieViewModel(id: movie.id, title: movie.title, image: movie.image)
})
}
// when a movie is picked, a new Step is emitted.
// That will trigger a navigation action within the WatchedFlow
public func pick (movieId: Int) {
self.steps.accept(DemoStep.movieIsPicked(withId: movieId))
}
}
4. 딥링킹(Deep Linking)사용
AppDelegate에서 FlowCoordinator에 도달하고 예를 들어 알림을 수신할 때 navigate(to:) 함수를 호출하여 사용할 수 있다.
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// example of how DeepLink can be handled
self.coordinator.navigate(to: DemoStep.movieIsPicked(withId: 23452))
}
참고
'RxSwift' 카테고리의 다른 글
RxViewController (0) | 2022.03.03 |
---|---|
Operator (0) | 2022.02.09 |
Subject (0) | 2022.02.08 |
Observable (0) | 2022.02.07 |
RxSwift란? (0) | 2022.02.07 |