RxSwift

RxFlow

motosw3600 2022. 3. 30. 18:36

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))
}

 

참고

https://github.com/RxSwiftCommunity/RxFlow

'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