본문 바로가기
🍎 iOS/Architecture

[RIBs] AddMemo 구현하기 (메모 추가하기)

by Fomagran 💻 2021. 8. 26.
728x90
반응형

안녕하세요 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배 이상이

 

걸렸던 것 같습니다.

 

분명 많은 팀원들과 규모가 큰 앱에서는 관리가 수월하고 협업하는데 도움이 될거라고 생각합니다.

 

하지만 단순하고 혼자 앱을 작업한다면 시간이나 파일 관리 등의 생산적인 측면에선 다른 디자인 패턴이 적합하다고

 

생각이 들었습니다.

728x90
반응형

댓글