안녕하세요 Foma👟 입니다!
오늘은 RIBs 아키텍처를 사용하여 화면 전환하는 법에 대해서 알아보도록 하겠습니다.
기존 Uber의 튜토리얼을 따라하려고 했으나... 저는 잘안되더라구요...
그래서 다른 좋은 튜토리얼이 없을까? 하고 열심히 찾아보다가 SimpleMemo를 RIBs로 구현한 튜토리얼 찾게되었습니다.
전 포스팅에서 RootRIB에서 LoggedOutRIB을 붙여 이동하고 LoggedOutRIB에서 간단한 비지니스 로직을 처리했는데요.
(혹시 저번 포스팅을 못보신 분들은 여기 에서 보고 와주세요!)
이어서 RootRIB에서 LoggedOutRIB을 떼고 LoggedInRIB을 붙이고 LoggedInRIB 하위에 있는 MemoRIB을
화면에 띄워보도록 하겠습니다.
상태 트리를 간략히 보여드리면 아래와 같습니다.
바로 시작할게요~
LoggedOut
저번 시간에 LoggedOutRIB에서 로그인 버튼을 누르면 Player1과 Player2의 이름을 출력하도록 했는데요.
이번에는 로그인 버튼을 누르면 LoggedOut이 dismiss되고 RootRIB으로 다시 돌아가는 것을 구현해보겠습니다.
LoggedOutInteractor
LoggedOutListener 프로토콜에 didLogin 함수를 정의해줍니다.
protocol LoggedOutListener: AnyObject {
func didLogin(player1Name:String,player2Name:String)
}
그리곤 LoggedOutInteractor의 handleLogind에서 listener를 통해 didLogin을 했다고 상위 RIB인 RootRIB에 알려줍니다.
func handleLogin(player1Name: String, player2Name: String) {
listener?.didLogin(player1Name: player1Name, player2Name: player2Name)
}
RootRouter
RootRouter엔 RootInteractable 프로토콜이 정의되어 있는데 여기서 LoggedOutListener를 채택하여
LoggedOutListener에서 발생한 이벤트를 RootIneractor에서 감지할 수 있도록 합니다.
protocol RootInteractable: Interactable, LoggedOutListener {
var router: RootRouting? { get set }
var listener: RootListener? { get set }
}
RootInteractor
RootInteractor로 가시면 아래와 같이 RootIneractable을 채택하고 있어요.
그러면 당연히 RootInteractable에서 채택한 LoggedOutListener의 메소드도 구현해줘야겠죠?
이미 정의해둔 didLogin이 자동완성 될거에요.
여기에 이제 로그인을 했을 때 어떤 것을 할지 구현해줘야 하는데요.
func didLogin(player1Name: String, player2Name: String) {
...
}
RootRouting에 로그인된 화면으로 인도하는 메소드를 정의해주겠습니다.
protocol RootRouting: ViewableRouting {
func routeToLoggedIn(player1Name:String,player2Name:String)
}
그리곤 didLogin 메소드에 위에서 정의해줬던 routeToLoggedIn을 실행시켜줍니다.
func didLogin(player1Name: String, player2Name: String) {
router?.routeToLoggedIn(player1Name: player1Name, player2Name: player2Name)
}
RootRouter
이제 RootRouter로 이동해서 routeToLoggedIn을 구현해줘야겠죠?
먼저 RootViewControllable 프로토콜에 화면을 띄우고 사라지게 하는 present,dismiss 메소드를 정의하겠습니다.
protocol RootViewControllable: ViewControllable {
func present(viewController: ViewControllable)
func dismiss(viewController:ViewControllable)
}
그리곤 RootRouter에 RootInteractor에서 실행시킨 routeToLoggedIn 메소드를 구현해줄건데요.
로그인 화면으로 이동하려면 먼저 기존에 붙여져있던 loggedOut 자식을 떼어줘야겠죠?
detachChild 메소드를 통해 떼어줍니다.
그리고 RootViewControllable 프로토콜에서 정의해둔 dismiss 메소드를 이용해서 loggedOut를 dismiss시켜주고
loggedOut을 nil로 만들어줍니다.
func routeToLoggedIn(player1Name: String, player2Name: String) {
if let loggedOut = loggedOut {
detachChild(loggedOut)
viewController.dismiss(viewController:loggedOut.viewControllable)
self.loggedOut = nil
}
}
이렇게 하면 로그인 버튼을 눌렀을 때 LoggedOut이 dismiss되고 RootRIB으로 되돌아가게 됩니다.
LoggedIn
이제 LoggedOutRIB을 떼어줬고 LoggedIn을 붙여줘야겠죠?
LoggedInRIB을 만들어줍니다.
주의하실 점은 LoggedIn은 어떤 것을 보여주는 것이 아닌 로그인한 상태만 알려주는 RIB이기 때문에 View를 갖지 않습니다.
고로 Owns corresponding view 체크를 해제해주고 만들어주세요.
이렇게 만들게 되면 아래와 같이 Router,Builder,Interactor 3개 파일만 생성될거에요.
RootComponent + LoggedIn
RootComponent + LoggedIn 이름으로 swift파일을 생성해주세요.
그리곤 RootComponent에 LoggedIn Dependency를 주입해주겠습니다.
LoggedInDependency를 채택할 땐 화면이 없는 RIB이기 떄문에 LoggedInViewController를 rootViewController로 지정해줘야 합니다.
(return rootViewController에서 오류가 날거에요. 아직 RootViewController에 LoggedInViewControllable을 채택하지 않았기 때문입니다.)
import RIBs
protocol RootDependencyLoggedIn: Dependency {
}
extension RootComponent: LoggedInDependency {
var LoggedInViewController: LoggedInViewControllable {
return rootViewController
}
}
RootViewController
RootViewController로 이동해서 LoggedInViewControllable을 확장시켜줍니다.
// MARK: LoggedInViewControllable
extension RootViewController: LoggedInViewControllable {
}
RootBuilder
Builder의 역할은 RIB을 생성해주는 것입니다.
고로 RootBuilder로 이동하여 LoggedInBuilder를 생성해줍니다.
(return RootRouter 부분에서 에러가 날거에요. 아직 RootRouter에 loggedInBuilder 초기화를 안해주었기 때문입니다.)
func build() -> LaunchRouting {
let viewController = RootViewController()
let component = RootComponent(dependency: dependency,
rootViewController: viewController)
let interactor = RootInteractor(presenter: viewController)
let loggedOutBuilder = LoggedOutBuilder(dependency: component)
let loggedInBuilder = LoggedInBuilder(dependency: component)
return RootRouter(interactor: interactor,
viewController: viewController,
loggedOutBuilder: loggedOutBuilder,
loggedInBuilder:loggedInBuilder)
}
RootRouter
RootRouter로 이동하여 loggedInBuilder를 선언해주시고
private let loggedInBuilder:LoggedInBuildable
loggedInBuilder를 초기화해줍니다.
init(interactor: RootInteractable,
viewController: RootViewControllable,
loggedOutBuilder: LoggedOutBuildable,
loggedInBuilder:LoggedInBuildable) {
self.loggedOutBuilder = loggedOutBuilder
self.loggedInBuilder = loggedInBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
그리곤 자식의 이벤트를 듣기 위해 RootIneractable LoggedInListener도 채택해줍니다.
protocol RootInteractable: Interactable, LoggedOutListener, LoggedInListener {
var router: RootRouting? { get set }
var listener: RootListener? { get set }
}
그리곤 routeToLoggedIn 함수에 loggedInRouting을 RootRIB의 자식으로 붙여줍니다.
func routeToLoggedIn(player1Name: String, player2Name: String) {
if let loggedOut = loggedOut {
detachChild(loggedOut)
viewController.dismiss(viewController:loggedOut.viewControllable)
self.loggedOut = nil
}
let loggedInRouting = loggedInBuilder.build(withListener: interactor)
attachChild(loggedInRouting)
}
이렇게 실행해보면 아무것도 일어나지 않을거에요.
왜냐하면 LoggedInRIB은 뷰가 없기 때문이죠.
Memo
LoggedInRIB의 자식인 MemoRIB을 만들어주겠습니다.
Memo는 메모 화면을 구성할 RIB입니다.
고로 뷰를 가지고 있고 스토리보드로 화면을 구성할 것이기 때문에
Owns corresponding view와 Adds Storyboard file을 체크해주세요.
LoggedInComponent + Memo
이제 MemoRIB을 LoggedInRIB의 자식으로 설정해줘야겠죠?
먼저 LoggedInComponent에 MemoDependency를 주입해주겠습니다.
import RIBs
protocol LoggedInDependencyMemo: Dependency {
}
extension LoggedInComponent: MemoDependency {
}
LoggedInBuilder
RootRIB에서 LoggedInRIB을 자식으로 설정했던 것과 같이 LoggedInBuilder로 이동해서 MemoRIB을 생성해줍니다.
func build(withListener listener: LoggedInListener) -> LoggedInRouting {
let component = LoggedInComponent(dependency: dependency)
let interactor = LoggedInInteractor()
interactor.listener = listener
let memoBuilder = MemoBuilder(dependency: component)
return LoggedInRouter(interactor: interactor,
viewController: component.LoggedInViewController,
memoBuilder:memoBuilder)
}
LoggedInRouter
LoggedInRouter에 memoBuilder를 초기화해줍니다.
final class LoggedInRouter: Router<LoggedInInteractable>, LoggedInRouting {
private let memoBuilder:MemoBuildable
// TODO: Constructor inject child builder protocols to allow building children.
init(interactor: LoggedInInteractable, viewController: LoggedInViewControllable,memoBuilder:MemoBuildable) {
self.viewController = viewController
self.memoBuilder = memoBuilder
super.init(interactor: interactor)
interactor.router = self
}
그리곤 LoggedInInteractable 프로토콜에 MemoListener를 채택해준 뒤
protocol LoggedInInteractable: Interactable,MemoListener {
var router: LoggedInRouting? { get set }
var listener: LoggedInListener? { get set }
}
LoggedInRouter에 didLoad 메소드를 만들어 로드되는 순간 MemoRIB을 붙여줍니다.
override func didLoad() {
super.didLoad()
let memoRouting = memoBuilder.build(withListener: interactor)
attachChild(memoRouting)
}
이제 MemoRIB의 화면을 보여줘야겠죠?
LoggedInViewControllable 프로토콜에 present 메소드를 정의합니다.
protocol LoggedInViewControllable: ViewControllable {
func present(viewController: ViewControllable)
}
다시 didLoad 메소드로 와서 memoRouting을 present 해주도록 구현해줍니다.
override func didLoad() {
super.didLoad()
let memoRouting = memoBuilder.build(withListener: interactor)
attachChild(memoRouting)
viewController.present(viewController: memoRouting.viewControllable)
}
MemoBuilder
이제 마지막으로 MemoBuilder로 이동해서 Storyboard를 불러오도록 구현해줍니다.
func build(withListener listener: MemosListener) -> MemosRouting {
let component = MemosComponent(dependency: dependency)
let viewController = UIStoryboard(name: "MemosViewController", bundle: nil).instantiateInitialViewController() as! MemosViewController
let interactor = MemosInteractor(presenter: viewController)
interactor.listener = listener
return MemosRouter(interactor: interactor, viewController: viewController)
}
StoryboardInstantiate
위와 같이 코드를 쓸 수도 있지만 조금 더 개선해주기 위해 Storyboard enum을 만들어 주겠습니다.
import UIKit
enum Storyboard: String {
case MemoViewController
func instantiate<VC: UIViewController>(_: VC.Type) -> VC {
guard let vc = UIStoryboard(name: self.rawValue, bundle: nil).instantiateInitialViewController() as? VC else {
fatalError("Storyboard \(self.rawValue) wasn`t found.")
}
return vc
}
}
MemoBuilder
다시 MemoBuilder로 돌아와 build의 코드를 개선해줍니다.
func build(withListener listener: MemoListener) -> MemoRouting {
let component = MemoComponent(dependency: dependency)
let viewController = MemoViewController.instantiate()
let interactor = MemoInteractor(presenter: viewController)
interactor.listener = listener
return MemoRouter(interactor: interactor, viewController: viewController)
}
MemoViewController
MemoViewController로 이동해 스토리보드를 instantiate를 해줍니다.
final class MemoViewController: UIViewController, MemoPresentable, MemoViewControllable {
static func instantiate() -> Self {
return Storyboard.MemoViewController.instantiate(self)
}
weak var listener: MemoPresentableListener?
}
그리곤 MemoViewController라는 것을 알려주기 위해 Label을 달아주겠습니다.
스토리보드에 label을 달아주고
MemoViewController에 연결해준 뒤
viewDidLoad 함수를 만들어 label의 텍스트를 "여기는 메모뷰컨트롤러 입니다!" 라고 알려주도록 하겠습니다.
final class MemoViewController: UIViewController, MemoPresentable, MemoViewControllable {
@IBOutlet weak var memoLabel: UILabel!
static func instantiate() -> Self {
return Storyboard.MemoViewController.instantiate(self)
}
override func viewDidLoad() {
memoLabel.text = "여기는 메모뷰컨트롤러 입니다!"
}
weak var listener: MemoPresentableListener?
}
실행화면
이렇게 실행해서 확인해보면 메모 뷰컨트롤러로 이동해되는 모습을 볼 수 있습니다!!
오늘은 이렇게 LoggedOutRIB에서 로그인 버튼을 누르면 LoggedOutRIB이 detach되고 RootRIB에서 LoggedInIRIB이
attach 되고 LoggedInRIB의 자식인 MemoRIB이 보여지는 것을 구현해보았습니다.
만약 기존의 MVC패턴으로 구현했다면 정말 간단하게 구현할 수 있는 화면 전환을 RIBs 아키텍처로 구현하니 정말 많은
파일이 생성되고 엄청 복잡하게 설계해야 화면 전환을 할 수 있었습니다.
하지만 그만큼 의존성이나 메모리 프로토콜 지향 프로그래밍을 하게 되어 안정성을 보장되는 설계를 할 수 있었습니다.
또한 RIB이 상태를 나열하고 붙였다 뗐다 하면서 상태를 이어가기 때문에 다른 사람과 협업 그리고 기능 확장 등을 할 때
유용하겠다라는 생각이 들었습니다.
혹시라도 틀린 점이 있거나 궁금한 점이 있다면 언제든 댓글로 알려주세요!
'🍎 iOS > Architecture' 카테고리의 다른 글
[RIBs] AddMemo 구현하기 (메모 추가하기) (0) | 2021.08.26 |
---|---|
[RIBs] Memo 삭제,수정 구현해보기 (0) | 2021.08.26 |
[RIBs] Interactor로 비니지스 로직 처리해보기 (feat. 초기세팅) (0) | 2021.08.18 |
[Design Pattern] RIBs란?(feat. Uber) (0) | 2021.08.18 |
[Design Pattern] MVP 패턴이란? (0) | 2021.08.11 |
댓글