안녕하세요 Foma 💻 입니다!
저번 시간엔 메모를 수정하고 삭제하는 것까지 구현했는데요.
오늘은 AddMemoViewController를 구현하고 메모를 추가하는 것을 구현해보도록 하겠습니다.
바로 시작할게요~
AddMemoRIB
스토리보드와 뷰컨트롤러를 포함한 AddMemoRIB을 만들어주세요.
AddMemoViewController.storyboard
AddMemoViewController는 숫자를 입력할 텍스트필드와 추가버튼을 눌러줍니다.
텍스트필드엔 숫자만 들어가야하므로 keyboard type을 numberPad로 만들어줍니다.
StoryboardInstantiate
새로운 스토리보드 뷰인 AddMemoViewController를 enum에 추가해줍니다.
enum Storyboard: String {
case MemoViewController
case AddMemoViewController
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
}
}
AddViewController
스토리보드로 구성되었으므로 AddViewController에 아래와 같이 넣어줍니다.
static func instantiate() -> Self {
return Storyboard.AddMemoViewController.instantiate(self)
}
스토리보드에서 텍스트필드와 버튼을 연결해줍니다.
@IBOutlet weak var addButton: UIButton!
@IBOutlet weak var numberTF: UITextField!
AddMemoPresentableListener에 메모가 추가될 액션을 미리 정의해둡니다.
protocol AddMemoPresentableListener: AnyObject {
func addMemo(number:Int)
}
addButton과 리스너의 addMemo 함수와 바인딩해줍니다.
private func bind() {
addButton.rx.controlEvent(.touchUpInside)
.subscribe{ [weak self] _ in
self?.listener?.addMemo(number: Int(self?.numberTF.text ?? "0") ?? 0)
}.disposed(by: disposeBag)
}
AddMemoBuilder
build함수에 AddMemoViewContoller를 instantiate해주어 스토리보드뷰컨트롤러로 만들어줍니다.
func build(withListener listener: AddMemoListener) -> AddMemoRouting {
let component = AddMemoComponent(dependency: dependency)
let viewController = AddMemoViewController.instantiate()
let interactor = AddMemoInteractor(presenter: viewController)
interactor.listener = listener
return AddMemoRouter(interactor: interactor, viewController: viewController)
}
AddInteractor
뷰컨트롤러에서 정의해둔 addMemo를 구현해줍니다.
메모가 추가되었을 때 MemoRIB의 데이터가 추가되어야하므로 부모RIB에 리스너를 통해서 알려줍니다.
//MARK:- AddMemoPresentableListener
extension AddMemoInteractor: AddMemoPresentableListener {
func addMemo(number: Int) {
listener?.addMemo(number: number)
}
}
위의 함수를 실행하기 위해선 미리 addMemo를 정의해두어야겠죠?
protocol AddMemoListener: AnyObject {
func addMemo(number:Int)
}
MemoViewController.storyboard
메모뷰컨트롤러 스토리보드에서 +버튼을 넣어주고 연결해줍니다.
@IBOutlet weak var addButton: UIBarButtonItem!
MemoPresentableListener에 +버튼을 눌렀을 때 AddMemo로 이동하는 함수를 정의해둡니다.
protocol MemoPresentableListener: AnyObject {
var memos: BehaviorRelay<[Memo]> { get }
func deleteMemo(_ index:Int)
func plusMemo(_ index: Int)
func moveToAddMemoButtonDidTap()
}
bind()함수에 addButton을 눌렀을 때 listener의 moveToAddMemoButtonDidTap() 실행되도록 합니다.
private func bind() {
...
addButton.rx.tap.subscribe(onNext: { [weak self] _ in
self?.listener?.moveToAddMemoButtonDidTap()
}).disposed(by: disposeBag)
}
MemoInteractor
addButton을 누르면 화면이 AddMemo로 이동해야하므로 router에 AddMemo로 이동해야 한다고 알려줘야합니다.
Routing 프로토콜에 moveToAddMemo()를 구현해줍니다.
protocol MemoRouting: ViewableRouting {
func moveToAddMemo()
}
이제 뷰컨트롤러에서 addButton을 눌렀을 때 route의 moveToAddMemo가 실행되도록 합니다.
// MARK: MemosPresentableListener
extension MemoInteractor: MemoPresentableListener {
...
func moveToAddMemoButtonDidTap() {
router?.moveToAddMemo()
}
}
그리곤 앞서 AddMemoInteractor에서 부모RIB에게 메모가 추가되었다고 알려주었죠?
addMemo를 구현해줍니다.
func addMemo(number: Int) {
var newMemos = memos.value
newMemos.append(Memo(number: number))
memos.accept(newMemos)
}
MemoRouter
MemoViewController 에서 AddMemoViewController로의 화면전환은 네비게이션으로 push 하겠습니다.
ViewControllable 프로토콜에 미리 정의해둡니다.
protocol MemoViewControllable: ViewControllable {
func push(viewController: ViewControllable)
}
addMemoBuilder와 addMemoRouting을 선언해줍니다.
addMemoBuilder는 MemoRIB에 자식으로 AddMemoRIB을 붙여하므로 미리 초기화해줍니다.
그리곤 moveToAdd 함수를 구현해주는데요.
addMemo를 자식으로 붙여주고 viewController를 push해줍니다.
final class MemoRouter: ViewableRouter<MemoInteractable, MemoViewControllable>, MemoRouting {
private let addMemoBuilder: AddMemoBuildable
private var addMemoRouting: AddMemoRouting?
init(interactor: MemoInteractable, viewController: MemoViewControllable,
addMemoBuilder:AddMemoBuildable) {
self.addMemoBuilder = addMemoBuilder
super.init(interactor: interactor, viewController: viewController)
interactor.router = self
}
func moveToAddMemo() {
let addMemoRouting = addMemoBuilder.build(withListener: interactor)
self.addMemoRouting = addMemoRouting
attachChild(addMemoRouting)
viewController.push(viewController: addMemoRouting.viewControllable)
}
}
MemoBuilder
MemoComponent에 AddMemoDependency를 주입해줍니다.
final class MemoComponent: Component<MemoDependency>,AddMemoDependency {
}
build함수에 addMemoBuilder를 Router 초기화에 추가해줍니다.
final class MemoBuilder: Builder<MemoDependency>, MemoBuildable {
...
func build(withListener listener: MemoListener) -> MemoRouting {
let component = MemoComponent(dependency: dependency)
let viewController = MemoViewController.instantiate()
let interactor = MemoInteractor(presenter: viewController)
interactor.listener = listener
let addMemoBuilder = AddMemoBuilder(dependency: component)
return MemoRouter(interactor: interactor, viewController: viewController,
addMemoBuilder:addMemoBuilder)
}
}
실행화면
메모뷰컨트롤러에서 +버튼을 눌러 AddMemo로 이동하고 숫자를 추가하면 MemoViewController 테이블뷰에
정상적으로 추가된 걸 볼 수 있습니다!
문제점
하지만 여기서 문제가 발생합니다.
AddMemo에서 Back 버튼을 누르면 네비게이션이 pop되어 MemoViewController로 이동하게 됩니다.
이 때 MemoRIB에 추가해준 AddMemoRIB이 detach되어야 하지만 그렇지 않아서 계속 붙어있는 상태로 존재합니다.
고로 AddMemo에서 Memo로 이동할 때 MemoRIB에서 AddMemoRIB을 떼어줘야합니다.
AddMemoViewController
AddMemoPresentableListener에 네비게이션 백버튼을 탭했을 때의 액션을 정의해줍니다.
protocol AddMemoPresentableListener: AnyObject {
func addMemo(number:Int)
func navigationBackDidTap()
}
그리곤 뷰가 사라질 때 리스너의 navigationBackDidTap이 실행되도록 합니다.
override func viewDidDisappear(_ animated: Bool) {
if isMovingFromParent {
listener?.navigationBackDidTap()
}
}
위에서 isMovingFromParent는 부모 뷰컨트롤에서 삭제되는지를 확인하는 것입니다.
AddMemoInteractor
Back버튼을 누르면 부모RIB에게 알려줘야 하므로 AddMemoListener에 navigationBack 을 정의해줍니다.
protocol AddMemoListener: AnyObject {
func addMemo(number:Int)
func navigationBack()
}
뷰컨트롤러에서 실행한 navigationBackDidTap() 로직을 작성해줍니다.
리스너를 통해서 부모RIB에게 알려줍니다.
//MARK:- AddMemoPresentableListener
extension AddMemoInteractor: AddMemoPresentableListener {
func addMemo(number: Int) {
listener?.addMemo(number: number)
}
func navigationBackDidTap() {
listener?.navigationBack()
}
}
MemoInteractor
메모 인터랙터에 MemoPresentableListener를 채택한 곳에 navigationBack을 구현해줍니다.
네비게이션이 Back되었으면 화면이 전환되므로 router에 전달해줍니다.
// MARK: MemosPresentableListener
extension MemoInteractor: MemoPresentableListener {
func deleteMemo(_ index:Int) {
var newMemos = memos.value
newMemos.remove(at: index)
memos.accept(newMemos)
}
func plusMemo(_ index: Int) {
var newMemos = memos.value
newMemos[index] = Memo(number:newMemos[index].number + 1)
memos.accept(newMemos)
}
func moveToAddMemoButtonDidTap() {
router?.moveToAddMemo()
}
func navigationBack() {
router?.backFromAddMemo()
}
}
메모 라우팅에 backFromAddMemo를 정의합니다.
protocol MemoRouting: ViewableRouting {
func moveToAddMemo()
func backFromAddMemo()
}
MemoRouter
메모 라우터로 이동해서 addMemo를 자식에서 떼어줍니다.
final class MemoRouter: ViewableRouter<MemoInteractable, MemoViewControllable>, MemoRouting {
...
func backFromAddMemo() {
guard let addMemoRouting = addMemoRouting else { return }
detachChild(addMemoRouting)
self.addMemoRouting = nil
}
}
Test
이제 Back버튼을 눌렀을 때 MemoRIB에서 AddMemoRIB이 떼어졌는지 확인해야겠죠?
확인하는 방법은 AddMemoInteractor의 willResignActive를 이용해서 resign되는지 확인해줍니다.
AddMemoInteractor
final class AddMemoInteractor: PresentableInteractor<AddMemoPresentable>, AddMemoInteractable {
...
override func willResignActive() {
super.willResignActive()
print("resign 될거에요~")
}
}
Back버튼을 누르면 아래와 같이 정상적으로 출력된 걸 볼 수 있습니다.
느낀점
이렇게 RIBs 아키텍처를 이용해서 메모앱의 화면전환,삭제,수정,추가 등을 구현해보았습니다.
굉장히 단순한 앱이었음에도 불구하고 화면전환이나 비지니스 로직을 작성하는데 약 30개의 파일과 복잡하고 많은
코드가 필요했습니다.
우선 많은 파일로 관리하게 되니 어떤 부분에서 어떤 로직이 실행되는지 분명하게 알 수 있는 장점이 있어고
복잡하게 관리되는 점에선 의존성 주입이나 로직 분리 등으로 상대적으로 안전한 앱을 만들 수 있었습니다.
처음 RIBs 아키텍처로 만들어서 시간이 더 들었겠지만 기존 MVC나 MVVM으로 만들 때보다 거의 3배 이상이
걸렸던 것 같습니다.
분명 많은 팀원들과 규모가 큰 앱에서는 관리가 수월하고 협업하는데 도움이 될거라고 생각합니다.
하지만 단순하고 혼자 앱을 작업한다면 시간이나 파일 관리 등의 생산적인 측면에선 다른 디자인 패턴이 적합하다고
생각이 들었습니다.
'🍎 iOS > Architecture' 카테고리의 다른 글
[Design Pattern] MVVM(Model - View - ViewModel) 패턴이란? (feat. Swift) (4) | 2021.10.19 |
---|---|
[iOS/Framework] Quick/Nimble이 뭘까? (feat. 사용법) (0) | 2021.08.31 |
[RIBs] Memo 삭제,수정 구현해보기 (0) | 2021.08.26 |
[RIBs] Router를 이용해 화면 전환해보기 (feat. Memo) (0) | 2021.08.24 |
[RIBs] Interactor로 비니지스 로직 처리해보기 (feat. 초기세팅) (0) | 2021.08.18 |
댓글