IAM iOS

[RxSwift] MVVM-C with Building Memo App (5) 메모 보기 구현 본문

RxSwift

[RxSwift] MVVM-C with Building Memo App (5) 메모 보기 구현

IAMiOS 2022. 5. 12. 01:10

 

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

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

 

메모 보기 구현


MemoDetailViewModel

  • 첫 번째 Cell에는 메모 내용, 두 번째 Cell에는 날짜 → [String] 방출
  • TableView에 데이터를 표시하기 위해 Observable과 Binding
  • Observable이 아닌 BehaviorSubject
    • 메모를 편집한 다음 보기 화면으로 오면 편집한 내용이 반영되어야 한다.
    • 이러기 위해서는 새로운 문자열 배열을 방출해야 한다.
import RxSwift
import RxCocoa
import Action

class MemoDetailViewModel: CommonViewModel {
    /// 이전 Scene에서 전달된 memo가 저장
    let memo: Memo

    private var formatter: DateFormatter = {
        let f = DateFormatter()
        f.locale = Locale(identifier: "Ko_Kr")
        f.dateStyle = .medium
        f.timeStyle = .medium
        return f
    }()

    var contents: BehaviorSubject<[String]>

    init(
        memo: Memo,
        title: String,
        sceneCoordinator: SceneCoordinatorType,
        storage: MemoStorageType
    ) {
        self.memo = memo

        contents = BehaviorSubject<[String]>(value: [
            memo.content,
            formatter.string(from: memo.insertDate)
        ])

        super.init(title: title, sceneCoordinator: sceneCoordinator, storage: storage)
    }
}

 

MemoDetailViewController

tableView와 Toolbar의 각 버튼 IBOutlet 연결

class MemoDetailViewController: UIViewController, ViewModelBindableType {

    var viewModel: MemoDetailViewModel!

    @IBOutlet weak var listTableView: UITableView!

    @IBOutlet weak var deleteButton: UIBarButtonItem!
    @IBOutlet weak var editButton: UIBarButtonItem!
    @IBOutlet weak var shareButton: UIBarButtonItem!

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

    func bindViewModel() {
        viewModel.title
            .drive(navigationItem.rx.title)
            .disposed(by: rx.disposeBag)

        viewModel.contents
            .bind(to: listTableView.rx.items) { tableView, row, value in
                switch row {
                case 0:
                    let cell = tableView.dequeueReusableCell(withIdentifier: "contentCell")!
                    cell.textLabel?.text = value
                    return cell
                case 1:
                    let cell = tableView.dequeueReusableCell(withIdentifier: "dateCell")!
                    cell.textLabel?.text = value
                    return cell
                default:
                    fatalError()
                }
            }
            .disposed(by: rx.disposeBag)
    }
}

 

화면 전환 MemoListViewController (각 cell(Memo) tap) → MemoDetailViewController

 

MemoListViewModel

MemoListViewModel에서 MemoDetailViewController로 전환하기 위한 Action 로직을 구현

  • 여기서는 메서드 형태가 아닌 속성 형태로 구현
  • 입력 형식은 Memo, 출력 형식은 Void
class MemoListViewModel: CommonViewModel {

			...

    lazy var detailAction: Action<Memo, Void> = {
        return Action { memo in
            let detailViewModel = MemoDetailViewModel(
                memo: memo,
                title: "메모 보기",
                sceneCoordinator: self.sceneCoordinator,
                storage: self.storage
            )

            let detailScene = Scene.detail(detailViewModel)
            return self.sceneCoordinator.transition(
                to: detailScene,
                using: .push,
                animated: true
            ).asObservable().map { _ in }
        }
    }()

 

MemoDetailViewController

  • zip → 선택된 Memo와 indexPath가 튜플 형태로 방출
    • TableView에서 Memo를 선택하면 ViewModel을 통해서 detail Action을 전달 (선택할 Memo 필요)
    • 선택한 Cell은 선택 해제 (indexPath가 필요)
  • do 연산자를 추가해서 next 이벤트가 전달되면 선택 상태 해제
  • 선택 상태를 처리하고 나서는 indexPath가 필요 없기 때문에, map 연산자로 데이터만 방출하도록 변경
  • 전달된 Memo를 detailAction과 Binding
class MemoListViewController: UIViewController, ViewModelBindableType {
			...	
	
    func bindViewModel() {
        Observable.zip(
            listTableView.rx.modelSelected(Memo.self),
            listTableView.rx.itemSelected
        )
        .do(onNext: { [unowned self] (_, indexPath) in
            /// do 연산자를 추가해서 next 이벤트가 전달되면 선택 상태 해제
            self.listTableView.deselectRow(at: indexPath, animated: true)
        })
        .map { $0.0 } /// 선택 상태를 처리하고 나서는 indexPath가 필요없기 때문에, map 연산자로 데이터만 방출하도록 변경
        .bind(to: viewModel.detailAction.inputs) /// 전달된 Memo를 detailAction과 Binding
        .disposed(by: rx.disposeBag)
    }
}

 

그런데 여기서 실행을 하게 되면 Memo List에서 Cell을 선택하면 아무런 반응이 없다.

SceneCoordinator에서 push case 블록에 들어오면 currentVC가 화면을 임베드하고 있는 UINavigationController가 저장되어 있다.

case .push:
    print(currnentVC) /// <UINavigationController: 0x7fa56003600>

    guard let nav = currentVC.navigationController else {
         subject.onError(TransitionError.navigationControllerMissing)
         break
    }

    nav.pushViewController(target, animated: animated)
    currentVC = target

    subject.onCompleted()

 

Scene으로 가서 list case를 보면 navigationController를 생성한 다음에 listVC가 아닌 navigationController를 return 한다.

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

 

SceneCoordinator로 돌아와서 list case를 보면 target에 전달된 scene을 생성하고, target을 그대로 저장하고 있는데 여기서 navigationController가 저장되어 있는 것이다.

  • navigationController가 다른 navigationController에 임베드되어있는 것이 아니기 때문에 push case에서 navigationController에 접근하면 nil이 return 되고, else 블록에서 종료가 된다.
let target = scene.instantiate()

case .root:
    currentVC = target
    window.rootViewController = target
    subject.onCompleted()

 

ViewController를 임베드하고있는 Controller가 아니라 실제 화면에 표시되어 있는 ViewController를 기준으로 전환을 처리해야 한다.
→ currentVC에 navigationController가 아닌 MemoListViewController가 저장되어 있어야 한다.

 


해결책!

실제로 화면에 표시되어있는 ViewController를 return 하는 속성을 추가

  • NavigationController와 같은 ContainerViewController라면 children를 return
  • 나머지는 self를 그대로 return 하게 수정
extension UIViewController {
    var sceneViewController: UIViewController {
        return self.children.first ?? self
    }
}
case .root:
    ...
    currentVC = target.sceneViewController
    ...
case .push:
    ...
    currentVC = target.sceneViewController
    ...
case .modal:
    ...
    currentVC = target.sceneViewController
    ...

 

그럼 이제 Cell(Memo)를 선택하면 Detail View로 넘어가지만 navigationItem의 backButton으로 pop 했을 때, 또다시 선택이 안된다.

SceneCoordinator

SceneCoordinator의 push case

  • delegate 메서드가 호출되는 시점마다 next 이벤트를 방출하는 Control event에 구독자를 추가하고, currentVC 속성을 업데이트
case .push:
		...

    nav.rx.willShow
        .subscribe(onNext: { [unowned self] event in
            self.currentVC = event.viewController.sceneViewController
        })
        .disposed(by: disposeBag)

 

 

결과 화면

 

 

코드 보러 가기

 

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

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

github.com

 

(6) 메모 편집 구현 보러 가기

 

[RxSwift] MVVM-C with Building Memo App (6) 메모 편집 구현

해당 포스팅은 KxCoding RxSwift 강의를 참고한 포스팅입니다! https://www.youtube.com/watch?v=m41N4czHGF4&list=PLziSvys01Oek7ANk4rzOYobnUU_FTu5ns&index=2 #7. 메모 편집 구현 편집 기능은 쓰기 기능과 동일..

llan.tistory.com