RxFlow

Zedd
14 min readApr 4, 2021

--

안녕하세요 :) Zedd입니다.

제가 Coordinator Pattern글을 미디엄에 썼더라구요..

RxFlow도 Coordinator Pattern을 코드로 녹여낸..그런 프레임워크니 이것도 미디엄에 써보겠습니다.

이 글을 읽기 전에, Coordinator Pattern글을 읽어보시는 것도 나쁘지 않을 것 같아요! 하하

# RxFlow

Reactive Flow Coordinator 패턴을 기반으로하는 / iOS 앱을 위한 navigation 프레임워크

iOS 앱 내 navigation에는 2가지 선택이 있음

1. Builtin mechanism : Apple 및 Xcode에서 제공하는 사용 — storyboards, segues

2. custom mechanism : 코드에서 직접 구현

[단점]

- Builtin mechanism : navigation은 상대적으로 정적(static)이고 스토리보드는 거대함(massive). navigation코드는 UIViewController를 오염시킴

- custom mechanism : 코드가 어려워질 수 있으며 선택한 디자인 패턴(Router, Coordinator)에 따라 복잡 할 수 있음.

# RxFlow의 목표

- UIViewController의 collaboration과 reusability를 가능하게 하기 위해 storyboards를 원자 단위로 컷팅함;;

- navigation context에 따라 다른 방식으로 UIViewController를 표시할 수 있게 됨

- 종속성 주입 구현 용이하게 했음

- UIViewController에서 모든 navigation mechanism 제거

- 대부분의 navigation case를 해결하면서 선언적 방식(declarative way)으로 navigation을 표현.

- 앱을 논리적 navigation block으로 쉽게 절단

# The key principles

RxFlow를 이해하기 위해 6가지 용어를 알아야함.

Flow : 각 Flow는 앱 내의 Navigation 영역을 정의. navigation action을 선언하는 곳.

Step : 각 Step은 앱의 navigation state. Flow와 Step의 조합은 가능한 모든 navigation action을 설명.
Step은 Flow에 선언된 화면으로 전파될 내부 값(ID, URL등)을 포함할 수 있음.

Stepper : Step을 생성할 수 있는 모든 것. Stepper는 Flow내 모든 navigation action을 트리거할 책임이 있음.

Presentable : 표현할 수 있는 것의 추상화(기본적으로 UIViewController와 Flow는 Presentable임). Presentable은 UIKit 호환 방식으로 Flow Step을 처리하기 위해 Coordinator가 구독할 Reactive observables을 제공한다.

Flowable : Presentable과 Stepper를 결합한 간단한 데이터 구조. Coordinator에게 Reactive mechanism안에서 새로운 Step을 생성할 다음 단계를 알려줌 (이건 뭔소린지 모르겠네)

Coordinator : 개발자가 navigation 가능성(possibilities)을 나타내는 Flow와 Step의 적절한 조합을 정의하면, Coordinator는 이러한 조합을 일관된 방식으로 혼합한다.

# RxFlow 예제 앱 살펴보기

RxFlow Part 1: In Theory내용은 끝났고, RxFlow 예제 앱을 보면서 공부하면 될 것 같습니다.

예제앱은 영화를 보여주고, 영화를 선택하면 상세 정보를 보여주는 앱입니다.

# Step정의하기

가장 먼저 Step을 정의해야하는 것 같습니다. Github에서도 Step을 가장 먼저 소개하기도 하구요.

RxFlow에서 Step은 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
}

Step을 정의할 때 가장 주의해야할 부분은, step case를 가능한 한 독립적으로 유지하는 것입니다.

예를 들어 showMovieDetail(withId: Int) 이런 case를 추가하는 것은,

영화를 선택 → 영화 세부 정보 화면을 보여줌

이 인과관계가 밀접하게 연결되기 때문에 나쁜생각일 수 있습니다.

navigate할 위치를 결정하는 것은 Step의 emitter에게 달려있지 않으며 이 결정은 Flow에 속하게 됩니다. (뭔소리..)

# Flow 정의하기

이제 해야할 일은 navigation의 기반이 될 Root Presentable을 선언하는겁니다.

navigate(to:) 함수를 구현하여 Step을 navigation actin으로 transform합니다.

Flow는 ViewController를 인스턴스화 할 때 dependency injection을 구현하는 데 사용할 수도 있습니다.

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

(RxFlow 예제앱에서는 main navigation을 나타내는 AppFlow를 포함하여 OnboardingFlow, DashboardFlow가 있습니다. )

# Stepper 정의하기

Stepper는 위에서 말했듯이

Step을 생성할 수 있는 모든 것.
Stepper는 Flow내 모든 navigation action을 트리거할 책임이 있음

Stepper는 ViewModel이 될 수도 있고, ViewController가 될 수 있습니다.

class TrendingViewModel: Stepper {    let steps = PublishRelay<Step>()
}
class SettingsAboutViewController: UIViewController, Stepper { let steps = PublishRelay<Step>()
}

Stepper는 steps 프로퍼티를 요구합니다. PublishRelay타입이므로 RxCocoa import가 필요합니다.

# 사용해보기

Login ↔️ Home Switch앱이라고 보면 될 것 같습니다.

위에서

  1. Step정의하기
  2. Flow정의하기
  3. Stepper정의하기

순으로 봤는데, 똑같이 해보겠습니다.

1. Step정의

enum DemoStep: Step {

case loginIsRequired
case homeIsRequired
}

RxFlow 예제 앱에 있는 DemoStep을 그대로 가져오되, 필요한 것만 남겼습니다.

2. Flow정의하기

위에서

RxFlow 예제앱에서는 main navigation을 나타내는 AppFlow를 포함하여 OnboardingFlow, DashboardFlow가 있습니다.

라고 했는데..AppFlow를 먼저 정의하겠습니다.

class AppFlow: Flow {

var root: Presentable

func navigate(to step: Step) -> FlowContributors {
// code
}
}

Flow프로토콜은 root와 navigate(to:)메소드를 요구하니 구현해주도록 하겠습니다.

[1] root 정의하기

AppFlow는 앱 전체 navigation을 control하고..

일반적인 샘플앱 상황에서는 UINavigationController가 root가 될 수 있겠지만..

Login ↔️ Home은 Window의 rootViewController가 교체되는게 자연스러울 것 같으니 window를 받도록 하겠습니다.

root는 Presentable타입이며 기본적으로 UIViewController는 Presentable이기 때문에 UIWindow역시 Presentable입니다.

[2] navigate(to: )정의하기

Step을 우리의 DemoStep으로 타입을 바꿔주고, enum이었으니 switch연산을 하면 됩니다.

login이 요구되면 login으로 navigate

home이 요구되면 home으로 navigate

navigateToLogin과 navigateToHome은 나중에 구현하도록 하겠습니다.

3. ViewController정의하기

1. Step정의하기 ✅

2. Flow정의하기 ✅

3. Stepper정의하기

원래같으면 Stepper를 정의할 차례인데요. 갑자기 ViewController를 정의한다니..

위에서 Stepper에 대해서 설명할 때 말했죠?

일단 Stepper의 정의부터 보겠습니다.

Stepper : Step을 생성할 수 있는 모든 것. Stepper는 Flow내 모든 navigation action을 트리거할 책임이 있음.

Stepper는 ViewModel이 될 수도 있고, ViewController가 될 수 있음.

각 ViewController에 있는 버튼을 클릭했을 때 이제 step이 일어나야하죠?

그래서 ViewController를 Stepper로 만들어 navigation action을 trigger해주도록 하겠습니다.

[LoginViewController]

[HomeViewController]

각 ViewController에서 주의깊게 봐야할 부분은,

steps를 trigger하는 곳입니다.

4. AppStepper정의하기

Q : 방금 Stepper작성했는데 왜 또 AppStepper를 작성??;;

A : 이것에 대한 정확한 이유는 잘 모르겠지만;;;

앱을 런치하면 일단은 어떤 화면으로 랜딩이 될거고, 그 랜딩은

enum DemoStep: Step {

case loginIsRequired
case homeIsRequired
}

Step에 정의되어있을겁니다. 어느 화면으로 일단 시작은 해야하잖아요?

그러니까 우리는 지금 “initialStep”이라는 것이 필요합니다.

Stepper를 준수하게 되면

class LoginViewController: UIViewController, Stepper {

var steps = PublishRelay<Step>()

var initialStep: Step { ✅
DemoStep.homeIsRequired
}
}

initialStep을 지정할 수 있는데요. (필수 프로퍼티는 아닙니다.)

이걸 ViewController에서 하지 않고, 따로 또 AppStepper라는 것을 두어 그 쪽에서 처리하도록 하게?

일단 지금 제가 봐도 ViewController에 initialStep을 정의한다는 것 자체가 좀 이상한 것 같아요..!?

AppStepper를 이렇게 구현해놓으면 특정 로직에 따라 initialStep을 바꿀 수도 있고..아무튼 ViewController에 정의하는게 이상하니..!! AppStepper를 무조건 작성해줍니다.

5. SceneDelegate 수정

RxFlow예제앱은 AppDelegate로 되어있으니 AppDelegate로 하고싶다면 그쪽 코드를 참고하면 될 것 같습니다.

저는 SceneDelegate이므로..

이렇게 해줍니다.

다른건 다 똑같지만..

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

flow navigation을 실행하겠다는 코드입니다.

6. LoginFlow, HomeFlow정의하기

우리는 지금 AppFlow만 정의했는데..Login과 Home이 다른 Flow를 타게하는 것이..제 목적이므로 각 Flow를 구현해줘야합니다.

[LoginFlow]

[HomeFlow]

7. AppFlow의 navigateTo~ 로직 추가

아까 navigateTo친구들은 나중에 구현해주겠다고 했는데,

이렇게 구현해줍니다.

  1. Flows.use는 특정 Flow를 사용하겠다~라고 보면 될 것 같습니다.
  2. OneStepper는 RxFlow에 구현된 빌트인 stepper라고 보면 됩니다. 단일 step을 내보낼 수 있습니다.

실행하면, 로그인 ↔️ 홈 스위치 앱이 만들어지게 됩니다.

오늘 프로젝트는 github에 올려놨으니 참고하시면 될 것 같아요.

코드를 봐도 잘 이해가 안가실 것 같은데요..

로그인 → 홈 부분만 그림으로 설명하면

이렇게 됩니다.

LoginViewController에서 loginButtonDidTap을 누르면

step이 trigger되고, 현재 LoginFlow를 사용중이므로 LoginFlow로 가게됩니다.

여기서 homeIsRequired case로 들어가게 되는데, Home으로 가려면 HomeFlow를 사용하도록 해야합니다.

즉, LoginFlow는 종료해야한다는 말이죠. 그래서 .end를 사용하여 현재 Flow를 종료해주는 코드라고 보면 됩니다.

현재 LoginFlow가 종료되고,

forwardToParentFlowWithStep

LoginFlow의 Parent인 AppFlow로 가라 && 근데 homeIsRequired step으로 가라

는 뜻으로 보면 될 것 같습니다.

그럼 AppFlow의

HomeIsRequired로 가게 되고, 결국 navigateHome이 실행되게 됩니다.

Home → Login도 똑같은 시나리오로 흘러가게 됩니다.

RxFlow가 처음이시고, 이 글만 보셨다면

진짜 하나도..진짜 단 1도 이해가 안가실수도 있을 것 같아요…ㅎㅎ..

저도 아직 잘 모르겠어서..best practice로 짠 건지 잘 모르겠습니다.

혹시 어색한 부분이 있다면 댓글 달아주세요! 🥲

RxFlow의 API가 엄청 다양한 것 같은데..이건 좀 써봐야 할 것 같네요.

# 후기

Reactive로 다 되니까..Coordinator Pattern에서 필수적인 Delegate 정의를 안해도 되는게 확실히 장점으로 다가오는 것 같습니다. (RxFlow도 이게 장점이라고 했구요)

참고

--

--