RxSwift) RxFlow 적용하기 (2)

Havi·2021년 4월 8일
4

RxFlow

목록 보기
2/3

제드님 블로그 참고
필권님 블로그 참고

Step 정의하기

Step은 어플리케이션의 네비게이션 상태라고 이전 글에서 설명했다.

일반적으로 Step을 가장 먼저 정의해주는 것 같다.

Step의 경우 enum으로 표현되며 case들을 가능한 독립적으로 유지해야한다.

이 말은 즉 coordinateToMovieDetail(withID: Int) 와 같은 코드는 MovieDetailVC와 밀접하게 연결되어 있기 때문에 독립적이지 않다.

따라서 movieIsPicked(withID: Int) 같이 표현해주는게 권장된다.

즉 선언형 프로그래밍 패러다임에 부합하도록 "어떠한 행동을 했다."로 정의해줘야 한다.

import RxFlow

enum SampleStep: Step {
    // Login
    case loginIsRequired
    case userIsLoggedIn
    
    // Home
    case dashboardIsRequired
}

Flow 정의하기

모든 네비게이션 코드(present, push)등은 Flow로 정의될 수 있다.

특정 Step과 합쳐질 경우 어떠한 네비게이션 행동을 일으킨다.

네비게이션의 기반이 될 Root Presentable을 선언해주고, navigate(to:) 함수를 구현하여 Step을 navigation action으로 transform한다.

Flow는 여러개가 정의될 수 있다.

내가 느끼기에는 Coordinator Pattern에서 Coordinator가 하는 역할을 Flow가 대신해준다고 생각된다.

먼저 AppFlow를 정의해보겠다.

앱이 처음 시작했을 때 어느 화면으로 가서 어느 Flow를 사용할지 결정해주는 Flow이다.

그렇다면 SceneDelegate(AppDelegate)에서 어떻게 시작하는지부터 알아보자.

SceneDelegate 코드

import UIKit

import RxFlow

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?
    var coordinator: FlowCoordinator = .init()

    func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        let window = UIWindow(windowScene: windowScene) 
        self.window = window
        
        let provider: ServiceProviderType = ServiceProvider()
        
        let appFlow = AppFlow(with: window, and: provider)
        
        coordinator.coordinate(flow: appFlow, with: AppStepper())
        window.makeKeyAndVisible()
    }

    func sceneDidDisconnect(_ scene: UIScene) {}
    func sceneDidBecomeActive(_ scene: UIScene) {}
    func sceneWillResignActive(_ scene: UIScene) {}
    func sceneWillEnterForeground(_ scene: UIScene) {}
    func sceneDidEnterBackground(_ scene: UIScene) {}
}

아마 이해가 안될 것이다.

요약하자면, AppStepper가 가지고 있는 Step을 받아서 AppFlow에 넘겨주고, AppFlow는 받은 Step과 Flow를 결합시켜 Navigation을 발생시킬 것이다.

그렇다면 AppStepper는 어떻게 생긴지 알아보자.

AppStepper 코드

import Foundation

import RxFlow
import RxRelay

struct AppStepper: Stepper {
    let steps: PublishRelay<Step> = .init()
    
    var initialStep: Step {
        return SampleStep.loginIsRequired
    }
}

나는 struct로 작성하였지만 필요에 따라 class, viewController로 사용하면 될 것 같다.

AppStepper에서는 initialStep으로 어느 step을 실행할 지 결정한다.

앱이 처음 켜진 상태이므로 loginIsRequired를 반환하겠다.

그렇다면 아까 SceneDelegate에서

coordinator.coordinate(flow: appFlow, with: AppStepper())

이 코드를 통해 appFlow에 step이 전달될 것이다.

이제 AppFlow 코드를 알아보자.

AppFlow 코드

import UIKit

import RxFlow

// Flow는 AnyObject를 준수하므로 class로 선언해주어야 한다.
final class AppFlow: Flow {
    private let rootWindow: UIWindow
    private let services: ServiceProviderType
    
    init(
        with window: UIWindow,
        and services: ServiceProviderType
    ) {
        self.rootWindow = window
        self.services = services
    }
    
    var root: Presentable {
        return self.rootWindow
    }
    
    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? SampleStep else {
            return FlowContributors.none
        }
        
        switch step {
        case .loginIsRequired:
            return coordinateToLoginVC()
            
        case .userIsLoggedIn, .dashboardIsRequired:
            return coordinateToMainVC()
        }
    }
    
    private func coordinateToLoginVC() -> FlowContributors {
//        if let rootVC = rootWindow.rootViewController {
//            rootVC.dismiss(animated: false)
//        }
        
        let loginFlow = LoginFlow(with: services)
        
        Flows.use(loginFlow, when: .created) { [unowned self] root in
            rootWindow.rootViewController = root
        }
        
        let nextStep = OneStepper(withSingleStep: SampleStep.loginIsRequired)
        
        return .one(flowContributor: .contribute(withNextPresentable: loginFlow, 
                                                 withNextStepper: nextStep))
    }
    
    private func coordinateToMainVC() -> FlowContributors {
        let homeFlow = HomeFlow(with: services)
        
        Flows.use(homeFlow, when: .created) { [unowned self] root in
            rootWindow.rootViewController = root
        }
        
        let nextStep = OneStepper(withSingleStep: SampleStep.dashboardIsRequired)
        
        return .one(flowContributor: .contribute(withNextPresentable: homeFlow,
                                                 withNextStepper: nextStep))
    }
    
}

AppFlow는 AppStepper에게 Step을 받아 navigate(to:) 함수를 실행시킨다.

.loginIsRequired를 전달했으므로 우리는 coordinateToLoginVC() 함수를 실행시킬 것이다.

coordinateToLoginVC() 에서는 AppFlow가 LoginFlow로 가는 것을 실행한다.

LoginFlow에 의존성을 주입해 객체를 생성하고, loginFlow가 .created 됐을 때 rootViewController에 loginFlow가 가지고 있는 rootVC(LoginVC)를 넣어준다.

그리고 flowContributor에 FlowContributors를 전달하면 loginFlow로 nextStep이 전달된다.

(내부 구현은 안까봐서 잘 모르겠다. 일단 전달된다고 이해하자.)

그렇다면 이제 LoginFlow는 어떻게 생겼을까?

LoginFlow

import UIKit

import RxFlow

final class LoginFlow: Flow {
    private lazy var rootViewController: UINavigationController = {
        let viewController = UINavigationController()
        return viewController
    }()
    
    var root: Presentable {
        return self.rootViewController
    }
    
    private let services: ServiceProviderType
    
    init(with services: ServiceProviderType) {
        self.services = services
    }
    
    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? SampleStep else { return .none }
        
        switch step {
        case .loginIsRequired:
            return coordinateToLogin()
            
        case .dashboardIsRequired, .userIsLoggedIn:
            return .end(forwardToParentFlowWithStep: SampleStep.dashboardIsRequired)
        }
    }
    
    private func coordinateToLogin() -> FlowContributors {
        let vm = LoginVM(with: services)
        let vc = LoginVC(with: vm)
        self.rootViewController.setViewControllers([vc], animated: false)
        return .one(flowContributor: .contribute(withNextPresentable: vc,
                                                 withNextStepper: vm))
    }
}

LoginFlow는 root로 네비게이션 컨트롤러를 가지고 있다.

AppFlow에서 온 Step을 받아 navigate(to:)함수를 실행시키고,

.loginIsRequired이 실행되었으니 coordinateToLogin() 함수를 실행시킨다.

coordinateToLogin()에서 loginVC를 생성하여 자신의 root에 넣어주고 FlowContributors반환하여 화면이동을 처리한다.

그렇다면 이제 LoginVC코드를 보자

Stepper 정의하기

LoginVC 코드

import UIKit

import RxFlow
import RxSwift
import RxCocoa

class LoginVC: UIViewController {
    
    private let viewModel: LoginVM
    var disposeBag = DisposeBag()
    
    var loginButton: UIButton = {
        let button = UIButton()
        button.setTitle("login", for: .normal)
        button.backgroundColor = .black
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    init(with viewModel: LoginVM) {
        self.viewModel = viewModel
        
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bind(with: viewModel)
        setUI()
    }
    
    func bind(with viewModel: LoginVM) {
        loginButton.rx.tap.subscribe(onNext: {
            viewModel.steps.accept(SampleStep.dashboardIsRequired)
        })
        .disposed(by: disposeBag)
        
        bindAction(with: viewModel)
        bindState(with: viewModel)
    }
}

private extension LoginVC {
    func setUI() {
    /// UI 코드
    }
}

LoginVC에서는 loginButton의 TapEvent를 받아 loginVM에 전달한다.

LoginVM 코드

import Foundation

import RxFlow
import RxSwift
import RxCocoa

struct LoginVM: ViewModelType, Stepper {
    var steps: PublishRelay<Step> = .init()
    
    // input, output 구현
    
    private let services: ServiceProviderType
    
    init(with services: ServiceProviderType) {
        self.services = services
    }
    
    // transform 구현

loginVM에서는 받은 SampleStep.dashboardIsRequired을 받는 steps와 Stepper 프로토콜이 구현되어있다.

steps는 SampleStep.dashboardIsRequired step을 accept한 뒤 LoginFlow에 step을 전달시킨다.

이로써 Login이 끝나고 MainVC로 이동할 수 있게된다.

Flow Diagram을 그려보았는데 맞는지는 모름

결론

여기까지 RxFlow에 대해 알아보았다.

전체 소스는 여기에서 볼 수 있다.

이해하는데에 정말 많은 시간이 들었고 작성해야할 코드의 량도 많았다.

그래서 내가 느끼는 장단점으로는

장점
1. VC, VM에서 화면 이동에 대한 로직을 분리시킬 수 있다.
2. 추상화를 통해 의존성을 정리하고 DI를 쉽게 할 수 있다.
3. 화면전환에 관한 테스트 작성이 쉬워진다.
4. RxFlow를 이해하고 있는 개발자라면 화면 전환이 어떻게 되는지 한눈에 파악하기 쉽다.

단점
1. 러닝커브가 상당히 많이 매우 정말 높다.
2. 화면 전환이 많지 않은 앱에서는 오버엔지니어링이 될 가능성이 높다.
3. 프레임워크를 적용하기 위한 보일러플레이트 코드가 존재한다.

로 정리해볼 수 있다.

이제 적용여부는 팀원과 상의해봐야겠다.

결론
데모로 몇가지 화면 적용해봤는데 필요이상으로 보일러플레이트가 많아져서 오히려 더 보기 힘들어짐.
화면 이동이 많은 앱이라면 분명한 장점이 있을듯 하지만, 우리 회사 앱은 화면이동이 많지않음
따라서 적용하지 않기로 함! ㅎ

끄-읏

profile
iOS Developer

0개의 댓글