개발/iOS

[iOS] RxFlow ?

allthi 2024. 11. 13. 14:45
반응형

RxFlow는 iOS 및 Swift 개발에서 RxSwift와 함께 사용되는 네비게이션 프레임워크로, 앱의 화면 전환과 흐름 제어를 반응형 프로그래밍 방식으로 관리할 수 있게 해줍니다. RxFlow는 화면의 흐름을 선언적으로 정의하여 복잡한 네비게이션 구조를 명확하게 하고, 유지보수를 용이하게 합니다.

주요 개념과 특징

  1. Flow와 Step 개념
    • Flow는 특정 화면 흐름이나 네비게이션 트리를 나타내며, 앱의 화면 흐름을 논리적으로 그룹화하는 단위입니다.
    • Step은 각 화면 전환을 정의하는 작은 단위로, 유저가 트리거한 액션이나 이벤트에 따라 새로운 화면으로 이동하는 단계입니다.
  2. 반응형 네비게이션
    • RxFlow는 RxSwift의 Observable을 통해 유저의 액션이나 특정 조건에 따라 화면 전환을 반응형으로 처리할 수 있습니다. 네비게이션 상태를 구독하고, 변화에 따라 화면 전환을 자동으로 수행합니다.
  3. 의존성 주입과 유지보수 용이성
    • 각 Flow는 독립적으로 작성되기 때문에 의존성 주입이 쉬우며, 유지보수가 용이합니다. 앱의 규모가 커지더라도 코드가 복잡해지지 않고 관리가 수월합니다.
  4. Declarative한 코드 스타일
    • 네비게이션 흐름을 선언적으로 작성하기 때문에, 코드가 읽기 쉽고 화면 전환 로직을 직관적으로 파악할 수 있습니다.

RxFlow를 사용하면 얻을 수 있는 이점

  • 화면 전환 로직과 비즈니스 로직의 분리로 코드 가독성 증가
  • 코드 중복을 줄여 앱의 확장성과 유지보수성 향상
  • 네비게이션 구조의 복잡성을 줄여 더 간결한 코드 작성 가능

RxFlow는 특히 복잡한 네비게이션 구조나 다양한 화면 전환을 요구하는 앱에서 매우 유용하게 사용할 수 있습니다.


예시

RxFlow를 사용하여 기본적인 네비게이션 플로우를 구현하는 예제를 살펴보겠습니다. 이 예제는 간단히 로그인 화면에서 메인 화면으로 이동하는 흐름을 관리하는 방식입니다.

먼저, RxFlow와 관련 라이브러리를 프로젝트에 설치합니다.

pod 'RxSwift'
pod 'RxCocoa'
pod 'RxFlow'

1. Flow 정의하기

Flow는 네비게이션의 흐름을 관리하는 핵심 구성 요소입니다. 각 Flow는 여러 Step을 포함하여 화면의 전환을 제어합니다.

import RxFlow
import RxSwift
import RxCocoa

class AppFlow: Flow {
    var root: Presentable {
        return self.rootViewController
    }
    
    private let rootViewController = UINavigationController()
    
    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? AppStep else { return .none }
        
        switch step {
        case .loginIsRequired:
            return navigateToLoginScreen()
        case .mainScreenIsRequired:
            return navigateToMainScreen()
        }
    }
    
    private func navigateToLoginScreen() -> FlowContributors {
        let viewController = LoginViewController()
        self.rootViewController.pushViewController(viewController, animated: true)
        return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController))
    }
    
    private func navigateToMainScreen() -> FlowContributors {
        let viewController = MainViewController()
        self.rootViewController.setViewControllers([viewController], animated: true)
        return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController))
    }
}

2. Step 정의하기

Step은 각 화면 전환의 단위를 정의합니다. 앱의 흐름에 따라 다양한 Step을 만들 수 있습니다.

enum AppStep: Step {
    case loginIsRequired
    case mainScreenIsRequired
}

3. ViewController와 Stepper 설정하기

각 ViewController는 Step을 방출하기 위해 Stepper를 사용합니다. Stepper는 Flow에 의해 구독되며, 사용자의 액션에 따라 Step을 트리거합니다.

import UIKit
import RxFlow
import RxSwift
import RxCocoa

class LoginViewController: UIViewController, Stepper {
    let steps = PublishRelay<Step>()
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        self.title = "Login"
        
        let loginButton = UIButton()
        loginButton.setTitle("Login", for: .normal)
        loginButton.addTarget(self, action: #selector(loginTapped), for: .touchUpInside)
        
        self.view.addSubview(loginButton)
        loginButton.center = self.view.center
    }
    
    @objc func loginTapped() {
        self.steps.accept(AppStep.mainScreenIsRequired)
    }
}
class MainViewController: UIViewController, Stepper {
    let steps = PublishRelay<Step>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .blue
        self.title = "Main"
    }
}

4. AppDelegate에서 FlowCoordinator 설정

FlowCoordinator는 모든 Flow와 Step을 관리하며, AppDelegate에서 설정하여 앱의 초기 흐름을 시작합니다.

import UIKit
import RxFlow

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let coordinator = FlowCoordinator()
    let disposeBag = DisposeBag()
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        
        let appFlow = AppFlow()
        coordinator.coordinate(flow: appFlow, with: OneStepper(withSingleStep: AppStep.loginIsRequired))
        
        window?.rootViewController = appFlow.root as? UINavigationController
        window?.makeKeyAndVisible()
        
        return true
    }
}

요약

이 예제는 다음과 같은 방식으로 동작합니다:

  1. 앱이 시작되면 AppFlow가 loginIsRequired Step으로 시작됩니다.
  2. AppFlow는 이 Step을 감지하여 LoginViewController로 이동합니다.
  3. 사용자가 로그인 버튼을 누르면, LoginViewController의 Stepper가 mainScreenIsRequired Step을 방출합니다.
  4. AppFlow는 이를 감지하고 MainViewController로 이동합니다.

이와 같은 방식으로, RxFlow는 반응형 네비게이션을 통해 복잡한 화면 전환 로직을 간결하게 작성할 수 있습니다.


활용 방법 상세 예시

RxFlow를 사용하여 10개의 화면을 만들고, 특정 조건에 따라 화면 전환을 제어하는 예제를 만들어 보겠습니다. 이번 예제에서는 각 화면이 1부터 10까지의 숫자를 가지고 있으며, 특정 조건에 따라 그룹 간 이동하는 방법을 포함하겠습니다.

전체 구조 요약

  1. 각 화면을 하나의 ViewController로 만들고, 화면마다 숫자를 표시합니다.
  2. Flow를 정의하여 화면 전환을 제어합니다.
  3. Step을 정의하여 각 화면을 나타내고 조건별 화면 전환을 제어합니다.

1. Step 정의하기

화면 전환 조건을 Step으로 정의하여 다양한 이동 조건을 설정합니다.

enum AppStep: Step {
    case moveToScreen(number: Int)
    case moveToFiveOrBelowGroup
    case moveToAboveFiveGroup
    case moveToOddGroup
    case moveToEvenGroup
}

2. Flow 정의하기

Flow는 각 화면의 흐름을 관리하며, AppFlow에서는 Step을 구독하고 각 조건에 맞는 화면 전환을 수행합니다.

import RxFlow
import RxSwift
import RxCocoa

class AppFlow: Flow {
    var root: Presentable {
        return self.rootViewController
    }
    
    private let rootViewController = UINavigationController()
    
    func navigate(to step: Step) -> FlowContributors {
        guard let step = step as? AppStep else { return .none }
        
        switch step {
        case .moveToScreen(let number):
            return navigateToScreen(number: number)
        case .moveToFiveOrBelowGroup:
            return navigateToGroup(isBelowFive: true)
        case .moveToAboveFiveGroup:
            return navigateToGroup(isBelowFive: false)
        case .moveToOddGroup:
            return navigateToGroup(isEven: false)
        case .moveToEvenGroup:
            return navigateToGroup(isEven: true)
        }
    }
    
    private func navigateToScreen(number: Int) -> FlowContributors {
        let viewController = NumberViewController(number: number)
        self.rootViewController.pushViewController(viewController, animated: true)
        return .one(flowContributor: .contribute(withNextPresentable: viewController, withNextStepper: viewController))
    }
    
    private func navigateToGroup(isBelowFive: Bool) -> FlowContributors {
        let start = isBelowFive ? 1 : 6
        let end = isBelowFive ? 5 : 10
        return .multiple(flowContributors: (start...end).map { number in
            let viewController = NumberViewController(number: number)
            return .contribute(withNextPresentable: viewController, withNextStepper: viewController)
        })
    }
    
    private func navigateToGroup(isEven: Bool) -> FlowContributors {
        let numbers = (1...10).filter { $0 % 2 == (isEven ? 0 : 1) }
        return .multiple(flowContributors: numbers.map { number in
            let viewController = NumberViewController(number: number)
            return .contribute(withNextPresentable: viewController, withNextStepper: viewController)
        })
    }
}

3. NumberViewController 구현하기

각 화면은 NumberViewController를 통해 구현됩니다. 이 컨트롤러는 숫자를 표시하고 그룹 이동 버튼을 제공합니다.

import UIKit
import RxFlow
import RxCocoa
import RxSwift

class NumberViewController: UIViewController, Stepper {
    let steps = PublishRelay<Step>()
    private let number: Int
    private let disposeBag = DisposeBag()
    
    init(number: Int) {
        self.number = number
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        self.title = "Screen \(number)"
        
        let fiveBelowButton = UIButton(type: .system)
        fiveBelowButton.setTitle("Five or Below", for: .normal)
        fiveBelowButton.rx.tap
            .map { AppStep.moveToFiveOrBelowGroup }
            .bind(to: steps)
            .disposed(by: disposeBag)
        
        let aboveFiveButton = UIButton(type: .system)
        aboveFiveButton.setTitle("Above Five", for: .normal)
        aboveFiveButton.rx.tap
            .map { AppStep.moveToAboveFiveGroup }
            .bind(to: steps)
            .disposed(by: disposeBag)
        
        let oddButton = UIButton(type: .system)
        oddButton.setTitle("Odd Numbers", for: .normal)
        oddButton.rx.tap
            .map { AppStep.moveToOddGroup }
            .bind(to: steps)
            .disposed(by: disposeBag)
        
        let evenButton = UIButton(type: .system)
        evenButton.setTitle("Even Numbers", for: .normal)
        evenButton.rx.tap
            .map { AppStep.moveToEvenGroup }
            .bind(to: steps)
            .disposed(by: disposeBag)
        
        let stack = UIStackView(arrangedSubviews: [fiveBelowButton, aboveFiveButton, oddButton, evenButton])
        stack.axis = .vertical
        stack.spacing = 20
        stack.alignment = .center
        
        view.addSubview(stack)
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        stack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

4. AppDelegate 설정하기

AppDelegate에서 FlowCoordinator를 설정하고 초기 화면을 moveToScreen(1) Step으로 지정합니다.

import UIKit
import RxFlow

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let coordinator = FlowCoordinator()
    let disposeBag = DisposeBag()
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        
        let appFlow = AppFlow()
        coordinator.coordinate(flow: appFlow, with: OneStepper(withSingleStep: AppStep.moveToScreen(number: 1)))
        
        window?.rootViewController = appFlow.root as? UINavigationController
        window?.makeKeyAndVisible()
        
        return true
    }
}

요약

이 예제에서는 다음과 같은 이동 기능이 제공됩니다:

  1. 특정 번호의 화면으로 이동 (moveToScreen).
  2. 5 이하 또는 6 이상 화면 그룹으로 이동 (moveToFiveOrBelowGroup, moveToAboveFiveGroup).
  3. 홀수 또는 짝수 화면 그룹으로 이동 (moveToOddGroup, moveToEvenGroup).

이 구조를 통해 화면 전환 로직을 선언적으로 정의할 수 있으며, 조건에 따라 원하는 그룹으로 쉽게 이동할 수 있습니다.


참고

네, 맞습니다! RxFlow는 일종의 라우터와 같은 역할을 합니다.

일반적으로 라우터는 화면 전환과 네비게이션을 담당하여 어떤 화면으로 이동할지 결정하고, 이를 제어하는 역할을 합니다. RxFlow 역시 앱의 화면 흐름을 관리하는 방식에서 라우터와 유사한 개념으로 작동합니다.

특히 RxFlow는 다음과 같은 라우터의 역할을 수행합니다:

  1. 화면 전환 제어: 각 Step을 통해 특정 조건에 따라 화면 전환을 수행합니다. 예를 들어, 특정 버튼을 클릭하면 관련 Step을 트리거하여 목표 화면으로 이동하게 합니다.
  2. 화면 이동 로직 분리: 각 화면 이동 로직을 Flow 내에 정의하여 앱의 네비게이션 흐름을 더 깨끗하게 관리할 수 있습니다. 덕분에 ViewController에 화면 전환 로직이 분리되어, 코드의 가독성과 유지보수성이 향상됩니다.
  3. 반응형 네비게이션: RxFlow는 RxSwift와 결합하여 반응형으로 화면 전환을 제어할 수 있게 합니다. 이는 이벤트 발생 시 화면 전환이 자연스럽게 이어지도록 만들어주어, 복잡한 네비게이션 흐름에서도 관리가 수월합니다.

따라서 RxFlow는 "반응형 라우터"로 이해할 수 있으며, 복잡한 화면 전환이 필요한 프로젝트에서 특히 유용하게 사용됩니다.

반응형