| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- flatMap
- 라이선스 저작권
- NewsApp
- 의존성 관리 도구
- IAMPopup
- swift
- 비동기(Async)
- Traits
- Dispatch Queue
- pagination
- Library
- RxSwift
- SPM
- Swift Package Manager
- MapKit
- Segmented Control
- 직렬(Serial)
- WeatherAPP
- cocoapods
- 동시(Concurrent)
- popupView
- ios
- NSCache
- 동기(Sync)
- Control Event
- OpenSource
- Multiple Cell Type
- MVVM
- Rxcocoa
- Transforming Operators
- Today
- Total
IAM iOS
[RxSwift] MVVM-C with Building Memo App (5) 메모 보기 구현 본문
해당 포스팅은 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
[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
'RxSwift' 카테고리의 다른 글
| [RxSwift] MVVM-C with Building Memo App (7) 메모 삭제 구현 (0) | 2022.05.12 |
|---|---|
| [RxSwift] MVVM-C with Building Memo App (6) 메모 편집 구현 (0) | 2022.05.12 |
| [RxSwift] MVVM-C with Building Memo App (4) 메모 쓰기 구현 (0) | 2022.05.12 |
| [RxSwift] MVVM-C with Building Memo App (3) 메모 목록 구현 (0) | 2022.05.12 |
| [RxSwift] MVVM-C with Building Memo App (2) Scene Coordinator (0) | 2022.05.12 |