
1. Builder
1) Builder 컴포넌트
Builder는 RIB의 본체라고 할 수 있는 해당 RIB의 Router를 만들고, 이를 위해 의존성을 외부에서 주입 받을 수 있는 컴포넌트 입니다.
public protocol Buildable: class {}
/// Utility that instantiates a RIB and sets up its internal wirings.
open class Builder<DependencyType>: Buildable {
/// The dependency used for this builder to build the RIB.
public let dependency: DependencyType
/// Initializer.
///
/// - parameter dependency: The dependency used for this builder to build the RIB.
public init(dependency: DependencyType) {
self.dependency = dependency
}
}
Buildable은 외부에서 Builder에 접근하기 위한 있는 추상화 된 클래스입니다.
예를 들면, 상위 Router에서 해당 RIB을 attach child 하기 위해서는 해당 RIB의 Buildable을 통해 build 메소드 등에 접근할 수 있는거죠.
이를 통해 해당 RIB의 Builder의 구현을 다 알지 못해도, 해당 RIB을 build 하는 작업을 수행할 수 있습니다.
예를 들어, 어떤 ViewController의 멤버 프로퍼티인 PageViewController에 어떤 RIB을 build 하여 붙인다고 해볼게요.
자식 RIB인 Actor RIB의 Builder가 이렇게 생겼다고 했을 때,
protocol ActorBuildable: Buildable {
func build(withListener listener: ActorListener, gender: Gender) -> ActorRouting
}
final class ActorBuilder: Builder<ActorDependency>,
ActorBuildable {
...
func build(
withListener listener: ActorListener,
gender: Gender
) -> ActorRouting {
...
return ActorRouter(
...
)
}
}
상위 RIB의 Router에서는 하위 RIB의 Buildable을 통해 해당 RIB을 생성하는 거죠.
Router가 하위 RIB의 Builder를 들고 있지 않고, Buildable 타입으로 들고 있다는 것은, Router가 하위 RIB의 Builder의 구현체에 의존하지 않는다는 것을 의미하기도 합니다.
즉, RIB 끼리 각자의 Interface로 통신하게 되는거죠.
final class ContainerRouter: ViewableRouter<ContainerInteractable, ContainerViewControllable>,
ContainerRouting {
private let actorBuilder: ActorBuildable
private var attachedRouters: [Routing] = []
...
func attachPages(tabInfos: [TabInfo]) {
let actorRouting = actorBuilder.build(
withListener: interactor,
gender: .male
)
attachChild(actorRouting)
attachedRouters.append(actorRouting)
...
}
...
}
여기서 중요한 지점이, Builder를 통해 의존성이 주입된다는 점인데,
Builder를 통해 의존성을 주입할 수 있는 방식은 두가지가 있습니다.
첫번째 방법은 Dependency를 통한 의존성 주입이고, 두번째는 build parameter를 통한 의존성 주입입니다.
Dependency를 통한 주입은 Static dependency를 주입할 때 쓰이고, build parameter를 통한 주입은 Dynamic dependency를 주입할 때 쓰입니다.
2) Dynamic(동적) / Static(정적) dependencies
동적 dependencies는 run time에서 결정되는 dependencies이고,
정적 dependencies는 compile time에서 결정되는 dependencies입니다.
RIBs 튜토리얼에 나온 예시를 통해 설명하면,
LoggedIn RIB에서는 더 상위 RIB에서 확정되지 않은 player1Name과 player2Name 값을 가지고 있기 때문에,
build 할 당시 정해지는 해당 String에 대해서 parameter로 의존성을 주입 받게 됩니다.
동적 의존성으로 주입 받게 되는 것이죠.
그리고 주입 받은 String 값은 Component에 저장되어 이후의 작업 (Interactor, VC, 하위 RIB) 등에서 사용되게 됩니다.
protocol LoggedInBuildable: Buildable {
func build(withListener listener: LoggedInListener,
player1Name: String,
player2Name: String) -> LoggedInRouting
}
func build(withListener listener: LoggedInListener,
player1Name: String,
player2Name: String) -> LoggedInRouting {
let component = LoggedInComponent(dependency: dependency,
player1Name: player1Name,
player2Name: player2Name)
...
}
final class LoggedInComponent: Component<LoggedInDependency>,
OffGameDependency {
var player1Name: String
var player2Name: String
init(
dependency: LoggedInDependency,
player1Name: String,
player2Name: String
) {
self.player1Name = player1Name
self.player2Name = player2Name
super.init(dependency: dependency)
}
}
반면에 OffGame의 경우
상위 RIB(LoggedIn)에서 player1Name과, player2Name에 대해 이미 확정된 String 값을 가지고 있기 때문에
즉, 현재 RIB을 기준으로 상위 RIB의 Component에 확정된 String 값이 있기 때문에 다음과 같이 Dependency를 통해 정적 의존성을 주입받을 수 있습니다.
protocol OffGameDependency: Dependency {
var player1Name: String { get }
var player2Name: String { get }
}
final class OffGameComponent: Component<OffGameDependency> {
fileprivate var player1Name: String {
return dependency.player1Name
}
fileprivate var player2Name: String {
return dependency.player2Name
}
}
어떤 dependencies를 어떻게 주입 받을 것인지는 해당 RIB을 중심으로 살펴보아야 합니다.
RIB이 attach 되는 시점, 즉 build가 될 때 주입 받아야 하는 의존성의 초기값이 이미 정해져 있다면 Dependency를 통해 주입 받으면 되고, 초기값이 정해져 있지 않은 채로, build 할 때의 상황에 따라 정해진다면 builder의 paraemter를 통해 주입 받으면 됩니다.
이를 위해 Dependency와 Component라는 도구가 존재합니다.
3) Dependency / Component
Dependency는 외부에서 해당 RIB으로 필요한 정보들을 나열해 놓은 명세에 불과합니다.
내부를 뜯어봐도 매우 간단하죠.
public protocol Dependency: class {}
/// The special empty dependency.
public protocol EmptyDependency: Dependency {}
상위 RIB에서는 해당 RIB을 attach 한다면, Component에 해당 Dependency를 제약 조건으로 추가하여, 필요한 정보를 명세를 통해 넘겨줍니다.
Component는 Dependency나 build 메소드의 parameter로 들어온 의존성들을 해당 RIB의 각각의 요소들이 사용할 형태,
그리고 자식 RIB의 Dependency에서 요구한 명세에 맞춰 데이터를 가공하여 각각의 요소에 뿌려주는 역할을 합니다.
open class Component<DependencyType>: Dependency {
public let dependency: DependencyType
public init(dependency: DependencyType) {
self.dependency = dependency
}
public final func shared<T>(__function: String = #function, _ factory: () -> T) -> T {
lock.lock()
defer {
lock.unlock()
}
if let instance = (sharedInstances[__function] as? T?) ?? nil {
return instance
}
let instance = factory()
sharedInstances[__function] = instance
return instance
}
// MARK: - Private
private var sharedInstances = [String: Any]()
private let lock = NSRecursiveLock()
}
Component는 기본적으로 Dependency를 멤버 변수로 가지고 있는데, 다음과 같이 dependency와 init parameter를 통해 동적, 정적 의존성 요소들을 이용하여 필요한 요소들을 구성합니다.
public final func shared<T>(__function: String = #function, _ factory: () -> T) -> T {
lock.lock()
defer {
lock.unlock()
}
if let instance = (sharedInstances[__function] as? T?) ?? nil {
return instance
}
let instance = factory()
sharedInstances[__function] = instance
return instance
}
// MARK: - Private
private var sharedInstances = [String: Any]()
sharedInstances, shared 함수는 연산 프로퍼티를 사용할 시, 한번 멤버 변수를 생성해서 사용했으면, 그 다음부터는 sharedInstances에 넣어두고 가져다 사용하는 용도를 위해 만들어져 있습니다.
다음과 같이 필요한 곳에서 사용할 수 있습니다.
하지만, 주의해야 할 점이 sharedInstances는 싱글톤으로 관리되는 것이 아니라, Component 인스턴스 마다 관리되기 때문에, Component가 새로 만들어진다면 sharedInstances도 새로 만들어진다는 점을 염두 하여야 합니다.
final class ContainerComponent: Component<ContainerDependency>,
ContainerInteractorDependency,
ActorDependency {
var mutableEditModeStream: MutableEditModeStream = EditModeStreamImpl(editMode: false)
var mutableStyleModeStream: MutableStyleModeStream = StyleModeStreamImpl(styleMode: .list)
var shouldShowDeleteAlertRelay: PublishRelay<Void> = .init()
var editModeStream: EditModeStream {
shared { mutableEditModeStream }
}
var styleModeStream: StyleModeStream {
shared { mutableStyleModeStream }
}
var shouldShowDeleteAlert: Observable<Void> {
shared { shouldShowDeleteAlertRelay.asObservable() }
}
let tabInfos: [TabInfo]
let initialIndex: Int
init(
dependency: ContainerDependency,
tabInfos: [TabInfo],
initialIndex: Int
) {
self.tabInfos = tabInfos
self.initialIndex = initialIndex
super.init(dependency: dependency)
}
}
4) Building
build 메소드의 예시입니다.
- parameter와 Dependency를 통해 Component를 생성
- View와 Interactor를 생성
- Interactor의 listener를 상위 RIB의 interactor로 설정
- Router 생성하여 반환
func build(
withListener listener: ActorListener,
gender: Gender
) -> ActorRouting {
let repository = PeopleRepositoryImp(
provider: MoyaProvider<RandomUserAPI>()
)
let component = ActorComponent(
dependency: dependency,
gender: gender,
repository: repository
)
let actorViewController = ActorViewController()
let actorInteractor = ActorInteractor(
presenter: actorViewController,
dependency: component
)
actorInteractor.listener = listener
let imageViewerBuilder = ImageViewerBuilder(
dependency: component
)
return ActorRouter(
interactor: actorInteractor,
viewController: actorViewController,
imageViewerBuilder: imageViewerBuilder
)
}
다음과 같이 반환된 Router는 부모 Router의 attachChild 메소드를 통해 child 배열에 들어가게 되고 생명주기가 시작되게 됩니다.
2. Router
1) Router 컴포넌트
라우터는 RIB의 본체라고 할 수 있을 정도로 RIB의 중심이 되는 컴포넌트입니다.
라우터가 하는 일은 크게 다음과 같습니다.
- RIB 트리 구성
- RIB의 Life Cycle을 관리
- interactor 관리
- (Viewable Router의 경우) View 관리
RIB의 생명 주기는 해당 RIB이 부모 RIB에 attach 될 때 시작되고, detach 될 때 종료됩니다.
먼저 눈여겨 보아야 할 부분은 Router와 ViewableRotuer의 멤버 프로퍼티입니다.
Router의 경우, children 이라는 Routing 배열을 가지는데, 이는 해당 RIB이 가지는 child RIB을 의미합니다.
또한 Interactable과 Interactor도 가지는 것을 알 수 있습니다.
여기 추가하여, ViewableRouter의 경우 viewControllable과 viewController도 가지고 있습니다.
open class Router<InteractorType>: Routing {
/// The corresponding `Interactor` owned by this `Router`.
public let interactor: InteractorType
/// The base `Interactable` associated with this `Router`.
public let interactable: Interactable
/// The list of children `Router`s of this `Router`.
public final var children: [Routing] = []
...
}
open class ViewableRouter<InteractorType, ViewControllerType>: Router<InteractorType>, ViewableRouting {
/// The corresponding `ViewController` owned by this `Router`.
public let viewController: ViewControllerType
/// The base `ViewControllable` associated with this `Router`.
public let viewControllable: ViewControllable
...
public init(interactor: InteractorType) {
self.interactor = interactor
guard let interactable = interactor as? Interactable else {
fatalError("\\(interactor) should conform to \\(Interactable.self)")
}
self.interactable = interactable
}
}
여기서 의문점이 드는 것은, RIB은 프로토콜 기반으로 각 컴포넌트들이 통신하는데 왜 interactor와 viewController를 직접 참조할 수 있도록 public으로 열어둔 걸까요??
일단 Lian님과 이야기를 나눠봤는데 상속받은 Router 구현체에서 interactor 자체를 재정의 하지 못하게 하려는 목적으로 public으로 열어둔게 아닌가 하는 예상을 해보았습니다.
private으로 가지고 있으면, 상속받는 쪽에서는 알지 못해서 마음대로 재정의가 가능해버리니까요ㅎㅎ

2월 21일 업데이트
이후 Lian님과 더 이야기해본 결과, 다른 RIB의 listener로 interactor의 구현체를 주입해야 하기 때문에, Interactor를 Router가 소유하고 있어야 한다는 결론을 내렸습니다.
너무 간단한 결론이었네요...
init(
interactor: RootInteractable,
viewController: RootViewControllable,
haksikBuilder: HaksikBuildable
) {
self.haksikBuilder = haksikBuilder
super.init(
interactor: interactor,
viewController: viewController
)
interactor.router = self
}
func attachHaksik(mealPlans: [MealPlan]) {
guard attachedRouter == nil
else { return }
let haksikRouter = haksikBuilder.build(
withListener: self.interactor, // <- interactor의 구현체를 주입한다.
mealPlans: mealPlans
)
self.attachChild(haksikRouter)
self.attachedRouter = haksikRouter
self.viewControllable.present(haksikRouter.viewControllable)
}
Router를 바라볼 때에는 부모 RIB에서 자식 RIB이 붙는 상황(attachChild) 부터 시작하여야 합니다.
그래야 Router의 생명주기가 시작하니까요!
2) Router의 Life cycle
Router의 life cycle의 종류는 didLoad 밖에 없습니다.
이는 load() 메소드에서 internalDidLoad() 메소드가 불리고 didLoad() 메소드가 불리기 직전에 didLoad가 방출되기 됩니다.
public enum RouterLifecycle {
/// Router did load.
case didLoad
}
/// The scope of a `Router`, defining various lifecycles of a `Router`.
public protocol RouterScope: class {
/// An observable that emits values when the router scope reaches its corresponding life-cycle stages. This
/// observable completes when the router scope is deallocated.
var lifecycle: Observable<RouterLifecycle> { get }
}
따로 life cycle을 챙기지 않아도 될 것 같은데, 이미 didLoad 라는 함수를 통해서 어떤 동작을 해줄 수 있기 때문에, 만약에 Load가 끝난 후에 어떤 동작을 해주고 싶으면 didLoad를 override 해서 사용하면 될 것 같습니다.
또한 activate 단계에서 초기화 되던 Interactor의 CompositeDisposable와는 달리, Rotuer의 CompositeDisposable는 router의 생성 시점에 init 되고, router의 deinit 시점에 dispose 됩니다.
즉, router에서 일어나는 구독 작업들은 router와 수명을 함께 합니다. (어차피 internal이라 RIBs 모듈 외부에서 사용하지는 못함)
3) Router의 attach / detach
RIB이 어떤 부모 RIB에 attach 되게 하려면 attachChild 메소드를 불러야 합니다.
public final func attachChild(_ child: Routing) {
assert(!(children.contains { $0 === child }), "Attempt to attach child: \\(child), which is already attached to \\(self).")
children.append(child)
// Activate child first before loading. Router usually attaches immutable children in didLoad.
// We need to make sure the RIB is activated before letting it attach immutable children.
child.interactable.activate()
child.load()
}
먼저 부모 Router의 children Routing배열에 child가 들어가게 되고요.
그 다음에 해당 RIB의 interactor를 activate 시킵니다. interactor의 생명 주기가 시작하는거죠.
private func bindSubtreeActiveState() {
let disposable = interactable.isActiveStream
.subscribe(onNext: { [weak self] (isActive: Bool) in
self?.setSubtreeActive(isActive)
})
_ = deinitDisposable.insert(disposable)
}
private func setSubtreeActive(_ active: Bool) {
if active {
iterateSubtree(self) { router in
if !router.interactable.isActive {
router.interactable.activate()
}
}
} else {
iterateSubtree(self) { router in
if router.interactable.isActive {
router.interactable.deactivate()
}
}
}
}
private func iterateSubtree(_ root: Routing, closure: (_ node: Routing) -> ()) {
closure(root)
for child in root.children {
iterateSubtree(child, closure: closure)
}
}
interactor를 active한 직후 load 메소드를 실행하는데 load 메소드에는 해당 RIB에 atttach되어 있는 모든 child RIB들의 interactor를 activate, deactivate 해주는 역할을 합니다.
상위 interactor가 activate 되면, 모든 연결된 하위 interactor를 activate 해주고, 그 반대의 경우에는 deactive 해주는 것이죠.
이렇게 되면 상위 Interactor와 그 하위 자식들의 Interactor의 생명 주기가 같아지게 됩니다.
만약에 didBecomeActive 단계에서 attach를 해준다면, interactor가 가장 하위 RIB에서 상위 RIB 순으로 activate 됩니다. 그 이유는 Attach 될 때, 자식 RIB부터 activate 하고 load 해주기 때문입니다.
즉, attach는 상위 RIB에서 하위 RIB 순으로 되지만, Interactor의 activate와 router의 load는 하위 RIB에서 상위 RIB 순으로 되더라고요.
load 시 interactable.isActiveStream subscribe 단계
2024-01-02 16:43:17.783: Sdaq.ActorInteractor -> subscribed
2024-01-02 16:43:17.784: Sdaq.ActorInteractor -> Event next(true)
2024-01-02 16:43:17.785: Sdaq.ActorInteractor -> subscribed
2024-01-02 16:43:17.785: Sdaq.ActorInteractor -> Event next(true)
2024-01-02 16:43:17.796: Sdaq.ContainerInteractor -> subscribed
2024-01-02 16:43:17.796: Sdaq.ContainerInteractor -> Event next(true)
2024-01-02 16:43:17.816: Sdaq.RootInteractor -> subscribed
2024-01-02 16:43:17.816: Sdaq.RootInteractor -> Event next(true)
4) ViewableRouter
Viewable Router 는 Router에 viewControllable이 포함되어 있는 클래스입니다.
마찬가지로 public으로 viewController의 구현체를 들고 있는 부분은 override를 방지하기 위해서라고 해석할 수도 있겠지만,
명확한 이유는 여전히 공부 중 입니다.
open class ViewableRouter<InteractorType, ViewControllerType>: Router<InteractorType>, ViewableRouting {
/// The corresponding `ViewController` owned by this `Router`.
public let viewController: ViewControllerType
/// The base `ViewControllable` associated with this `Router`.
public let viewControllable: ViewControllable
Viewable한 Router는 초기화 파라미터로 ViewController을 주어야 합니다.
3. Interactor
1) Interactor 컴포넌트
Interactor는 RIB의 비즈니스 로직을 담당하는 컴포넌트로, Interactor의 생명주기는 Router가 담당하고 있습니다.
attachChild를 통해 상위 RIB에 붙게 되면, interactable이 activate 되고, detach 될 때 deactivate 되는 방식으로 생명 주기를 가지고 있죠!
Interactor가 수행하는 모든 작업은 해당 Interactor의 수명 주기 내에서 한정되어야 합니다.
Interactor가 deactivate 된 상태에서 구독이 계속 실행되어서 비즈니스 로직이나 UI가 업데이트 되면 안되기 때문입니다.
또한, PresentableInteractor는 ViewController와 Presentable과 PresentableListener 인터페이스를 통해 서로 통신을 할 수 있습니다.
2) Activate / Deactivate
Interactor에서는 Activate와 Deactivate만 눈여겨서 보면 됩니다.
모든 로직은 activate 단계에서 일어나는 didBecomeActive에 정의 될 것이고,
모든 구독은 deactivate 단계에서 dispose 됨으로서 해제되기 때문이죠! (물론 deinit 단계에서도 deactivate가 불립니다.)
public final func activate() {
guard !isActive else {
return
}
activenessDisposable = CompositeDisposable()
isActiveSubject.onNext(true)
didBecomeActive()
}
public final func deactivate() {
guard isActive else {
return
}
willResignActive()
activenessDisposable?.dispose()
activenessDisposable = nil
isActiveSubject.onNext(false)
}
deinit {
if isActive {
deactivate()
}
isActiveSubject.onCompleted()
}
Active 단계에서는 activenessDisposable이 초기화 됩니다.
activenessDisposable는 interactor의 활성 상태와 수명을 같이하는 disposeBag의 개념으로 생각하시면 됩니다.
후에 조금 더 자세히 설명할게요!
그리고 isActiveSubject 를 true로 만들어주고, 우리가 override 하여 비즈니스 로직을 심는 didBecomeActive() 메소드가 실행되게 됩니다.
deactivate 단계에서는 우리가 VC에서 viewWillDisappear처럼 사용했던 willResignActive() 메소드가 실행됩니다. 그리고 activenessDisposable이 dispose 되고 해제되며 isActiveSubject가 false로 바뀌게 됩니다.
isActiveSubject를 다음과 같이 외부에서 읽게 한 것은 Router에서 RIB이 연결되었는데, interactor가 deactive 상태이면 active 상태로 만들어줄 수 있도록 하기 위함이고,
public이기 때문에 RIBs 모듈 외부에서도 interactor의 활성 상태를 읽거나 추적할 수 있습니다.
/// Indicates if the interactor is active.
public final var isActive: Bool {
do {
return try isActiveSubject.value()
} catch {
return false
}
}
/// A stream notifying on the lifecycle of this interactor.
public final var isActiveStream: Observable<Bool> {
return isActiveSubject.asObservable().distinctUntilChanged()
}
3) disposeOnDeactivate
@discardableResult
func disposeOnDeactivate(interactor: Interactor) -> Disposable {
if let activenessDisposable = interactor.activenessDisposable {
_ = activenessDisposable.insert(self)
} else {
dispose()
print("Subscription immediately terminated, since \\(interactor) is inactive.")
}
return self
}
disposeOnDeactivate을 사용하면, interactor에 있는 구독의 수명을 주입 받은 interactor의 활성 상태와 동기화시킬 수 있습니다.
interactor가 deactivate 되면 구독이 해제되는 거죠.
@discardableResult 키워드 덕분에 함수는 다음과 같이 사용 가능합니다.
dependency.styleModeStream.styleModeObservable
.distinctUntilChanged()
.bind { [weak self] _ in
self?.presenter.didChangeStyleMode()
}
.disposeOnDeactivate(interactor: self)
'Swift 아키텍처' 카테고리의 다른 글
[ReactorKit] (2) Reactorkit 내부 구조를 뜯어보고 사용하자 (0) | 2024.03.03 |
---|---|
[Reactorkit] (1) Reactorkit 도입기와 여전히 남은 고민들 (0) | 2023.08.06 |
[MVC] iOS의 MVC 아키텍처에 대해서 (0) | 2022.09.06 |
[Redux] Redux를 이용한 상태관리 후기 (with SwiftUI) (2) | 2022.05.15 |
[Redux] SwiftUI에서 Redux 아키텍처 적용해보기 - 첫인상 (0) | 2022.04.18 |