IAM iOS

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

RxSwift

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

IAMiOS 2022. 5. 12. 01:01

 

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

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

 

메모 쓰기 구현


MemoComposeViewModel

  • 저장과 취소 코드를 직접 구현을 하면 ViewModel에서 처리방식이 하나로 고정되는데, 파라미터로 받으면 이전 화면에서 처리 방식을 동적으로 결정할 수 있다는 장점이 있다.
  • saveAction과 cancelAction은 파라미터에서 옵셔널로 정의하여 실제로 Action이 일어날 때만 실행(execute)할 수 있게 래핑 해준다.
import RxSwift
import RxCocoa
import Action

class MemoComposeViewModel: CommonViewModel {
    /// Compose Scene에 표시할 메모를 저장할 속성
    /// 새로운 메모는 nil, 편집할 때는 편집할 메모가 저장
    private let content: String?

    /// Driver: View에 Binding할 수 있도록
    var initialText: Driver<String?> {
        return Observable.just(content).asDriver(onErrorJustReturn: "")
    }

    /// 저장/취소 두가지 Action을 구현 (Action 저장 속성)
    /// navigationBar에 저장 버튼과 취소 버튼에 두 Action(save, cancel)과 Binding
    let saveAction: Action<String, Void>
    let cancelAction: CocoaAction

    /// 저장과 취소 코드를 직접 구현을 하면 ViewModel에서 처리방식이 하나로 고정
    /// 파라미터로 받으면 이전 화면에서 처리 방식을 동적으로 결정할 수 있다는 장점
    init(
        title: String,
        content: String? = nil,
        sceneCoordinator: SceneCoordinatorType,
        storage: MemoStorageType,
        saveAction: Action<String, Void>? = nil,
        cancelAction: CocoaAction? = nil
    ) {
        self.content = content

        /// saveAction을 옵셔널로 선언하고 한번 더 래핑하여
        /// 실제로 Action이 전달되었다면 실행(execute) 후 화면을 닫음(close)
        /// Action이 전달되지 않았다면 화면만 닫음(close)
        self.saveAction = Action<String, Void> { input in
            if let action = saveAction {
                action.execute(input)
            }
            return sceneCoordinator.close(animated: true).asObservable().map { _ in }
        }

        /// cancelAction도 동일
        self.cancelAction = CocoaAction {
            if let action = cancelAction {
                action.execute(())
            }
            return sceneCoordinator.close(animated: true).asObservable().map { _ in }
        }
        
        super.init(title: title, sceneCoordinator: sceneCoordinator, storage: storage)
    }
}

 

MemoComposeViewController

  • 해당 ViewController에서는 새로운 메모 추가, 메모 편집을 다룬다.
  • 취소 버튼을 Tap 하면 cancelAction에 래핑되어있는 코드 실행
  • 저장버튼을 Tap하면 TextView에 저장된 문자열을 저장

- tap 속성에 Binding 해준 뒤, 더블 탭을 막기 위해 throttle 연산자로 0.5초마다 한 번씩 Tap을 처리
- withLatestFrom 연산자로 TextView에 입력된 Text를 방출
- 방출된 Text를 saveAction과 Binding

import RxSwift
import RxCocoa
import Action
import NSObject_Rx

class MemoComposeViewController: UIViewController, ViewModelBindableType {

    var viewModel: MemoComposeViewModel!

    @IBOutlet weak var cancelButton: UIBarButtonItem!
    @IBOutlet weak var saveButton: UIBarButtonItem!
    @IBOutlet weak var contentTextView: UITextView!

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

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        contentTextView.becomeFirstResponder()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if contentTextView.isFirstResponder {
            contentTextView.resignFirstResponder()
        }
    }

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

        /// 쓰기모드에서는 빈 문자열, 편집모드에서는 편집할 문자열 표시
        viewModel.initialText
            .drive(contentTextView.rx.text)
            .disposed(by: rx.disposeBag)

        /// 취소버튼을 Tap하면 cancelAction에 래핑되어있는 코드 실행
        cancelButton.rx.action = viewModel.cancelAction

        /// 저장버튼을 Tap하면 TextView에 저장된 문자열을 저장
        saveButton.rx.tap
            .throttle(.milliseconds(500), scheduler: MainScheduler.instance) /// 더블 탭 방지
            .withLatestFrom(contentTextView.rx.text.orEmpty) /// TextView에 입력된 Text를 방출
            .bind(to: viewModel.saveAction.inputs) /// 방출된 text를 saveAction과 Binding
            .disposed(by: rx.disposeBag)
    }
}

 

화면 전환 MemoListViewController (+버튼) → MemoComposeViewController

 

MemoListViewModel

  • +버튼과 Binding 할 Action 구현
    • MemoStorageType에서 createMemo(content: String) 메서드 호출 (Memo 생성)
    • flatMap 연산자로 화면 전환 처리
  • Compose Scene으로 전환하기 위해 sceneCoordinator.transition메서드 호출
    • Scene 파라미터에 들어갈 Scene 타입과 Scene의 연관 값인 ViewModel(MemoComposeViewModel) 생성하여 저장
    • transition 메서드는 Completable을 return 하기 때문에 map 연산자로 Void 형식을 방출하는 Observable로 변환해서 return (.asObservable().map { _ in })
  • MemoComposeViewModel의 init 파라미터의 saveAction과 cancelAction을 처리하기 위한 메서드 생성 (performUpdate / performCancel)
    • MemoStorageType을 update / delete (생성된 메모 업데이트 / 삭제)
    • createMemo가 실행되면 메모가 새로 생성되고 Observable로 방출되기 때문

 

saveAction
  • 입력 타입: String → 입력값(input)으로 Memo를 업데이트
  • update는 편집된 메모를 방출 / Observable이 방출하는 형식은 Void로 방출하는 형식이 다르기 때문에 map 연산자로 해결 (cancelAction도 동일)
import Action

class MemoListViewModel: CommonViewModel {
		...

    /// saveAction 처리 메서드
    /// 생성된 메모 업데이트: createMemo가 실행되면 메모가 새로 생성되고 Observable로 방출되기 때문
    func performUpdate(memo: Memo) -> Action<String, Void> {
        /// 입력 타입: String -> 입력값으로 Memo를 업데이트
        return Action { input in
            /// update: 편집된 메모를 방출 / Observable이 방출하는 형식은 Void
            /// 방출하는 형식이 다르기 때문에 map 연산자로 해결
            return self.storage.update(memo: memo, content: input).map { _ in }
        }
    }

    /// cancelAction 처리 메서드
    /// 생성된 메모 삭제: createMemo가 실행되면 메모가 새로 생성되고 Observable로 방출되기 때문
    func performCancel(memo: Memo) -> CocoaAction {
        return Action {
            return self.storage.delete(memo: memo).map { _ in }
        }
    }

    /// +버튼과 Binding할 Action 구현
    func makeCreateAction() -> CocoaAction {
        return CocoaAction { _ in
            return self.storage.createMemo(content: "")
                .flatMap { memo -> Observable<Void> in
                    /// 화면 전환 처리
                    let composeViewModel = MemoComposeViewModel(
                        title: "새 메모",
                        sceneCoordinator: self.sceneCoordinator,
                        storage: self.storage,
                        saveAction: self.performUpdate(memo: memo),
                        cancelAction: self.performCancel(memo: memo)
                    )

                    /// Compose Scene 생성 후, 연관 값 ViewModel 저장
                    /// transition 메서드는 Completable을 return하기 때문에 
                    /// map 연산자로 Void 형식을 방출하는 Observable로
                    let composeScene = Scene.compose(composeViewModel)
                    return self.sceneCoordinator.transition(
                        to: composeScene,
                        using: .modal,
                        animated: true
                    ).asObservable().map { _ in }
                }
        }
    }
}

 

MemoListViewController

func bindViewModel() {
    ...
    addButton.rx.action = viewModel.makeCreateAction()
}

 

결과 화면

 

 

코드 보러 가기

 

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

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

github.com

 

(5) 메모 보기 구현 보러 가기

 

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

해당 포스팅은 KxCoding RxSwift 강의를 참고한 포스팅입니다! https://www.youtube.com/watch?v=m41N4czHGF4&list=PLziSvys01Oek7ANk4rzOYobnUU_FTu5ns&index=2 #6. 메모 보기 구현 MemoDetailViewModel 첫 번째..

llan.tistory.com