IAM iOS

[RxSwift] MVVM-C with Building Memo App (2) Scene Coordinator 본문

RxSwift

[RxSwift] MVVM-C with Building Memo App (2) Scene Coordinator

IAMiOS 2022. 5. 12. 00:43

 

해당 포스팅은 KxCoding RxSwift 강의를 참고한 포스팅입니다!

https://www.youtube.com/watch?v=m41N4czHGF4&list=PLziSvys01Oek7ANk4rzOYobnUU_FTu5ns&index=2

 

Scene 구성


ViewModelBindableType

Protocol
  • MVVM 패턴으로 구현할 때는 ViewModel을 ViewController의 속성으로 추가
  • ViewModel과 View를 Binding
  • ViewModel의 타입은 ViewController마다 달라지기 때문에 Generic Protocol로 선언
  • Protocol은 associatedtype 키워드를 사용해서 관련 타입(associatedtype)을 선언
  • 관련 타입(associatedtype)은 제네릭 매개변수 절에 있는 타입 매개변수와 유사하다.
  • Protocol 선언 부의 where 절에서 제네릭을 사용할 수 있다.
protocol ViewModelBindableType {
    associatedtype ViewModelType

    var viewModel: ViewModelType! { get set }
    func bindViewModel()
}

 

extension
  • ViewController에 추가된 ViewModel의 실제 속성을 저장
  • bindViewModel 메서드를 자동으로 추가하는 메서드 구현

where Self

  • associatedtype의 사용 시 재정의가 필요 없도록 where절에 준수해야 할 프로토콜 제약을 명시해서 associatedtype에 대한 제약을 주도록 할 수 있다.
  • 해당 Protocol의 extension을 특정 Protocol을 상속했을 때만 사용될 수 있도록 하는 제약조건 추가 기능
  • 즉, 해당 extension에서 만든 메서드와 프로퍼티는 UIViewController를 상속받지 않은 곳에서는 사용할 수가 없다.
extension ViewModelBindableType where Self: UIViewController {
    mutating func bind(viewModel: Self.ViewModelType) {
        self.viewModel = viewModel
        loadViewIfNeeded()

        bindViewModel()
    }
}

 

MemoListViewController

  • Memo List, Detail, Compose → ViewController, ViewModel 생성
  • ViewModelBindableType 프로토콜 채택
import UIKit

class MemoListViewController: UIViewController, ViewModelBindableType {

    var viewModel: MemoListViewModel!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    func bindViewModel() { /// ViewModelBindableType

    }
}

 

 

Scene Coordinator


TransitionModel

/// 전환 방식 표현
enum TransitionStyle {
    case root
    case push
    case modal
}

/// 전환 시 발생할 에러 타입
enum TransitionError: Error {
    case navigationControllerMissing
    case cannotPop
    case unknown
}

 

Scene

  • 앱에서 구현할 Scene
  • Scene과 연관된 ViewModel을 연관 값으로 저장
enum Scene {
    case list(MemoListViewModel)
    case detail(MemoDetailViewModel)
    case compose(MemoComposeViewModel)
}

/// 스토리보드에 있는 Scene을 생성
extension Scene {
    func instantiate(from storyboard: String = "Main") -> UIViewController {
        let storyboard = UIStoryboard(name: storyboard, bundle: nil)

        /// 연관값에 저장된 ViewModel을 Binding해서 return
        switch self {
        case .list(let viewModel):
            guard let nav = storyboard.instantiateViewController(
                withIdentifier: "ListNav"
            ) as? UINavigationController else { fatalError() }

            guard var listVC = nav.viewControllers.first as? MemoListViewController
            else { fatalError() }

            /// 실제 Scene과 ViewModel을 Binding하고 NavigationController를 return
            listVC.bind(viewModel: viewModel)
            return nav

        case .detail(let viewModel):
            guard var detailVC = storyboard.instantiateViewController(
                withIdentifier: "DetailVC"
            ) as? MemoDetailViewController else { fatalError() }

            detailVC.bind(viewModel: viewModel)
            return detailVC

        case .compose(let viewModel):
            guard let nav = storyboard.instantiateViewController(
                withIdentifier: "ComposeNav"
            ) as? UINavigationController else { fatalError() }

            guard var composeVC = nav.viewControllers.first as? MemoComposeViewController
            else { fatalError() }

            composeVC.bind(viewModel: viewModel)
            return nav
        }
    }
}

 

SceneCoordinatorType

  • SceneCoordinator가 공통적으로 구현해야 하는 멤버 선언

Completable(Traits)

  • 구독자를 추가하고, 화면 전환이 완료된 후에 원하는 작업을 구현할 수 있다.
  • 이런 작업이 필요없다면 사용하지 않아도 된다.
  • @discardableResult를 선언했기 때문에 경고는 표시되지 않는다.
import RxSwift

protocol SceneCoordinatorType {
    /// 새로운 Scene을 표시
    @discardableResult
    func transition(
        to scene: Scene,
        using style: TransitionStyle,
        animated: Bool
    ) -> Completable

    /// 현재 Scene을 닫고 전 Scene으로 돌아가기
    @discardableResult
    func close(animated: Bool) -> Completable
}

 

SceneCoordinator

  • SceneCoordinatorType 프로토콜을 채택
  • SceneCoordinator는 화면 전환을 담당하기 때문에 window 인스턴스현재 화면에 표시되어 있는 Scene(currentVC)을 가지고 있어야 한다.
import RxSwift
import RxCocoa

class SceneCoordinator: SceneCoordinatorType {
    private let disposeBag = DisposeBag()

    /// SceneCoordinator는 화면 전환을 담당하기 때문에 window 인스턴스와
    /// 현재 화면에 표시되어 있는 Scene을 가지고 있어야한다.
    private var window: UIWindow
    private var currentVC: UIViewController

    required init(window: UIWindow) {
        self.window = window
        currentVC = window.rootViewController!
    }

    @discardableResult
    func transition(
        to scene: Scene,
        using style: TransitionStyle,
        animated: Bool
    ) -> Completable {
        /// 전환 결과를 방출할 subject
        let subject = PublishSubject<Void>()

        /// Scene을 생성해서 상수에 저장
        let target = scene.instantiate()

        /// TransitionStyle에 따라 실제 전환 처리
        switch style {
        case .root:
            /// rootViewController를 바꿔주면 된다.
            currentVC = target
            window.rootViewController = target

            /// subject로 completed 이벤트 전달
            subject.onCompleted()
        case .push:
            /// navigationController에 임베드되어있을 때만
            /// 아니라면 error이벤트를 전달하고 중지
            guard let nav = currentVC.navigationController else {
                subject.onError(TransitionError.navigationControllerMissing)
                break
            }

            /// navigationController에 임베드되어있다면
            /// Scene을 push하고 completed 이벤트 전달
            nav.pushViewController(target, animated: animated)
            currentVC = target

            subject.onCompleted()
        case .modal:
            currentVC.present(target, animated: animated) {
                subject.onCompleted()
            }
            currentVC = target
        }

        return subject.ignoreElements().asCompletable()
    }

    @discardableResult
    func close(animated: Bool) -> Completable {
        return Completable.create { [unowned self] completable in
            if let presentingVC = self.currentVC.presentingViewController {
                self.currentVC.dismiss(animated: animated) {
                    self.currentVC = presentingVC
                    completable(.completed)
                }

            } else if let nav = self.currentVC.navigationController {
                guard nav.popViewController(animated: animated) != nil else {
                    completable(.error(TransitionError.cannotPop))
                    return Disposables.create()
                }
                self.currentVC = nav.viewControllers.last!
                completable(.completed)

            } else {
                completable(.error(TransitionError.unknown))
            }

            return Disposables.create()
        }
    }
}

 

 

코드 보러 가기

 

GitHub - camosss/RxSwift: RxSwift 공부한 내용 정리

RxSwift 공부한 내용 정리. Contribute to camosss/RxSwift development by creating an account on GitHub.

github.com

 

(3) 메모 목록 구현 보러 가기

 

[RxSwift] MVVM-C with Building Memo App (3) 메모 목록 구현

해당 포스팅은 KxCoding RxSwift 강의를 참고한 포스팅입니다! https://www.youtube.com/watch?v=m41N4czHGF4&list=PLziSvys01Oek7ANk4rzOYobnUU_FTu5ns&index=2 #4. 메모 목록 구현 CommonViewModel ViewModel 의..

llan.tistory.com