RIBs 유래
VIPER
- VIPER는 View, Interactor, Presenter, Entity, Router로 구성
- VIPER는 View Driven이기 때문에 비즈니스 로직만 있는 VIPER 노드 구현에 어려움이 있음
RIBs
- RIBs는 Router, Interactor, Builder의 약자
- 하나의 RIB은 Router, Interactor, Builder는 필수적으로 구성된다
- 필요에 따라 Presenter와 View까지 추가적으로 구성한다
- VIPER와 달리 View가 포함되지 않고 비즈니스 로직으로만 구성된 Viewless RIB 생성이 가능하다
RIBs 구성
Builder
- RIBs의 모든 구성 요소를 생성
- Router, Interactor, Presenter, View, Component 모두 생성
Dependency
- Dependency는 RIB이 올바르게 초기화 되기 위해 부모로부터 필요로 하는 의존성을 나열하는 프로토콜
Component
- Component는 Dependency 프로토콜을 구현하는 클래스
- Component는 RIB의 Builder에 부모 의존성을 제공하는 것 외에도, RIB이 자체적으로 생성하거나 자식 RIB을 위해 생성하는 의존성을 소유하는 역할도 담당
Router
- 자식 RIB을 attach 혹은 detach하여 RIBs의 논리적 트리 구조 형성
Interactor
- 비즈니스 로직을 수행하여 Router로 라우팅 호출
- RIBs의 attach와 detach를 요청
- Presenter로 data model을 전달
View
- UI를 생성하고 구성
- UI 이벤트를 Presenter로 전달
- ViewModel을 받아서 UI를 업데이트
Presenter
- Interactor와 View간의 통신을 담당
- Business Model을 ViewModel로 변환하는 역할
- 상태를 갖고 있지 않은 클래스
- Presenter를 생략하는 경우 ViewModel 변환은 View 또는 Interactor가 함
RIBs 트리
- RIBs는 트리 구조를 형성하여, 부모 RIB과 자식 RIB 간 통신을 함
- RIBs간의 통신은 각 RIB의 Interactor가 담당
- 부모에서 자식으로의 통신은 Stream을 넘겨주어서 데이터를 전달
- 자식에서 부모로의 통신은 자식 Interactor에서 부모 Interactor를 Listener로 접근
RIBs 예시
Board RIB
- 게시판의 게시물이 리스트 형태로 표시되는 화면
PostDetail RIB
파일 구조
- Board, PostDetail RIBs의 파일 구조는 다음과 같다
Board RIB
BoardBuilder
protocol BoardDependency: Dependency {
...
}
final class BoardComponent: Component<BoardDependency>, PostDetailDependency {
let postsStream: BehaviorRelay<(posts: [Post], initial: Bool, reload: Bool)> = .init(value: (posts: [], initial: true, reload: true))
}
protocol BoardBuildable: Buildable {
func build(withListener listener: BoardListener) -> ViewableRouting
}
final class BoardBuilder: Builder<BoardDependency>, BoardBuildable {
func build(withListener listener: BoardListener) -> ViewableRouting {
let component = BoardComponent(dependency: dependency)
let viewController = BoardViewController()
let interactor = BoardInteractor(
presenter: viewController,
postsStream: component.postsStream
)
interactor.listener = listener
let postDetailBuilder = PostDetailBuilder(dependency: component)
return BoardRouter(
interactor: interactor,
viewController: viewController,
postDetailBuilder: postDetailBuilder
)
}
}
- BoardBuilder는 RIB의 모든 구성요소를 생성하고 의존성을 주입하는 역할
- postsStream을 통해 게시물 데이터를 관리하고 필요하다면 자식 RIB과 공유
- 자식 RIB인 PostDetailBuilder를 생성하는 역할까지 함
BoardInteractor
protocol BoardRouting: ViewableRouting {
func routeToPostDetail(post: PostEntity)
func detachPostDetail()
}
protocol BoardPresentable: Presentable {
var listener: BoardPresentableListener? { get set }
func update(posts: [Post], initial: Bool, reload: Bool)
}
protocol BoardListener: AnyObject {
...
}
final class BoardInteractor: PresentableInteractor<BoardPresentable>, BoardInteractable {
weak var router: BoardRouting?
weak var listener: BoardListener?
private let disposeBag = DisposeBag()
private let postsStream: BehaviorRelay<(posts: [Post], initial: Bool, reload: Bool)>
private var selectedBoardId: String?
...
init(
presenter: BoardPresentable,
postsStream: BehaviorRelay<(posts: [Post], initial: Bool, reload: Bool)>
) {
self.postsStream = postsStream
super.init(presenter: presenter)
presenter.listener = self
}
override func didBecomeActive() {
super.didBecomeActive()
Task {
do {
try await fetchPosts(initial: true)
} catch {
logError("Error fetching initial data: \(error)")
}
}
postsStream
.subscribe(onNext: { [weak self] posts in
self?.presenter.update(posts: posts.posts, initial: posts.initial, reload: posts.reload)
})
.disposed(by: disposeBag)
}
}
extension BoardInteractor {
private func fetchPosts(initial: Bool = false, reload: Bool = true) async throws {
let endpoint = PostEndpoint.fetchPosts(boardId: selectedBoardId, reload: reload)
let response: FetchPostsResponse = try await Network.shared.request(endpoint: endpoint)
let posts = response.data
self.postsStream.accept((posts: posts, initial: initial, reload: reload))
}
}
extension BoardInteractor: BoardPresentableListener {
func postDetailButtonDidTap(post: PostEntity) {
router?.routeToPostDetail(post: post)
}
}
extension BoardInteractor: PostDetailListener {
func detachPostDetail() {
router?.detachPostDetail()
}
}
- 게시판의 비즈니스 로직을 담당
- 게시물 목록을 fetch 하는 네트워크 요청 처리
- postsStream을 구독하여 데이터 변경 시 View 업데이트
- PostDetail RIB 라우팅 호출
BoardViewController
protocol BoardPresentableListener: AnyObject {
func postDetailButtonDidTap(post: PostEntity)
}
final class BoardViewController: UIViewController {
weak var listener: BoardPresentableListener?
private var posts = [PostEntity]()
...
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
...
}
extension BoardViewController: BoardPresentable {
func update(posts: [Post], initial: Bool, reload: Bool) {
Task {
self.posts = try await withThrowingTaskGroup(of: PostEntity.self) { group in
...
}
await MainActor.run {
...
}
}
}
}
extension BoardViewController: BoardViewControllable {
func push(_ viewControllable: ViewControllable, animated: Bool) {
DispatchQueue.main.async {
self.navigationController?.pushViewController(viewControllable.uiviewController, animated: animated)
}
}
func pop(_ viewControllable: ViewControllable, animated: Bool) {
DispatchQueue.main.async {
viewControllable.uiviewController.navigationController?.popViewController(animated: animated)
}
}
}
extension BoardViewController: UITableViewDelegate, UITableViewDataSource {
...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
...
let post = posts[indexPath.row]
listener?.postDetailButtonDidTap(post: post)
}
}
- 게시판 UI를 담당하며, UITableView를 통해 게시물 목록 표시
- NavigationController를 통해 화면 이동 기능 구현 (push / pop)
- Interactor와 통신하여 사용자 이벤트 전달
BoardRouter
protocol BoardInteractable: Interactable, PostDetailListener {
var router: BoardRouting? { get set }
var listener: BoardListener? { get set }
}
protocol BoardViewControllable: ViewControllable {
func push(_ viewControllable: ViewControllable, animated: Bool)
func pop(_ viewControllable: ViewControllable, animated: Bool)
}
final class BoardRouter: ViewableRouter<BoardInteractable, BoardViewControllable> {
private let postDetailBuilder: PostDetailBuildable
private var postDetailRouter: PostDetailRouting?
init(
interactor: BoardInteractable,
viewController: BoardViewControllable,
postDetailBuilder: PostDetailBuildable
) {
self.postDetailBuilder = postDetailBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
}
extension BoardRouter: BoardRouting {
func routeToPostDetail(post: PostEntity) {
let router = postDetailBuilder.build(withListener: interactor, post: post)
postDetailRouter = router
attachChild(router)
viewController.push(router.viewControllable, animated: true)
}
func detachPostDetail() {
guard let router = postDetailRouter else { return }
viewController.pop(router.viewControllable, animated: true)
detachChild(router)
postDetailRouter = nil
}
}
- Board RIB과 PostDetail RIB 간의 라우팅 담당
- PostDetail RIB의 attach 및 detach 관리
PostDetail RIB
PostDetailBuilder
protocol PostDetailDependency: Dependency {
...
}
final class PostDetailComponent: Component<PostDetailDependency> {
...
}
protocol PostDetailBuildable: Buildable {
func build(withListener listener: PostDetailListener, post: PostEntity) -> PostDetailRouting
}
final class PostDetailBuilder: Builder<PostDetailDependency>, PostDetailBuildable {
override init(dependency: PostDetailDependency) {
super.init(dependency: dependency)
}
func build(withListener listener: PostDetailListener, post: PostEntity) -> PostDetailRouting {
let component = PostDetailComponent(dependency: dependency)
let viewController = PostDetailViewController()
let interactor = PostDetailInteractor(presenter: viewController, post: post)
interactor.listener = listener
return PostDetailRouter(interactor: interactor, viewController: viewController)
}
}
PostDetailInteractor
protocol PostDetailRouting: ViewableRouting {
...
}
protocol PostDetailPresentable: Presentable {
var listener: PostDetailPresentableListener? { get set }
func update(post: PostEntity)
}
protocol PostDetailListener: AnyObject {
func detachPostDetail()
}
final class PostDetailInteractor: PresentableInteractor<PostDetailPresentable>, PostDetailInteractable {
weak var router: PostDetailRouting?
weak var listener: PostDetailListener?
private let post: PostEntity
init(presenter: PostDetailPresentable, post: PostEntity) {
self.post = post
super.init(presenter: presenter)
presenter.listener = self
}
override func didBecomeActive() {
super.didBecomeActive()
presenter.update(post: post)
}
}
extension PostDetailInteractor: PostDetailPresentableListener {
func backButtonDidTap() {
listener?.detachPostDetail()
}
}
- 게시물 상세 화면의 비즈니스 로직 담당
- 게시물 데이터를 View에 전달
- PostDetail RIB 라우팅을 부모 RIB에 호출
PostDetailViewController
protocol PostDetailPresentableListener: AnyObject {
func backButtonDidTap()
}
final class PostDetailViewController: UIViewController {
weak var listener: PostDetailPresentableListener?
...
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
}
extension PostDetailViewController {
...
@objc private func backButtonTapped() {
listener?.backButtonDidTap()
}
}
extension PostDetailViewController: PostDetailViewControllable {
...
}
extension PostDetailViewController: PostDetailPresentable {
func update(post: PostEntity) {
}
}
- 게시물 상세 화면 UI 담당하여, 게시물 정보 표시
- 뒤로가기 버튼 등 UI 이벤트 처리
PostDetailRouter
protocol PostDetailInteractable: Interactable {
var router: PostDetailRouting? { get set }
var listener: PostDetailListener? { get set }
}
protocol PostDetailViewControllable: ViewControllable {
...
}
final class PostDetailRouter: ViewableRouter<PostDetailInteractable, PostDetailViewControllable>, PostDetailRouting {
override init(interactor: PostDetailInteractable, viewController: PostDetailViewControllable) {
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
}
- PostDetail RIB의 라우팅 담당
- 필요한 경우 추가적인 자식 RIB 연결 가능
참고