RxSwift와 MVVM을 프로젝트에 적용시키기 위해 다양한 글들을 읽어봤는데 가장 보편적으로 사용되는 패턴이 Input/Output 패턴인 것 같았다.
해당 패턴은 두 가지 방법으로 구현이 가능했는데
첫 번째는 ViewModel 내에서 Input, Output 객체를 생성하고 View에서 바인딩하는 방법
두 번째는 View에서 Input 객체를 생성하고 ViewModel의 transform의 함수를 호출해 Output 객체를 반환받아 사용하는 방법이 있었다.
개인적으로 View에서는 ViewModel의 데이터에 직접 접근해 수정이 가능하도록 만드는 것이 MVVM과는 살짝 거리가 있다고 생각해서 두 번째 방식을 선택했고 코드는 아래와 같다.
// ViewModel.swift
protocol ViewModel {
associatedtype Input
associatedtype Output
var disposeBag: DisposeBag { get set }
func transform(input: Input) -> Output
}
ViewModel이라는 이름의 Protocol을 생성한다.
Input과 Output의 형태를 ViewModel 내에서 정의할 수 있도록 associatedType으로 지정하고 transform 함수를 구현하도록 선언한다.
// MainChatViewModel.swift
class MainChatViewModel: ViewModel {
struct Input {
let inputMessage: Observable<String>
let tapSend: Observable<Void>
let tapChatTextField: Observable<Void>
}
struct Output {
let replyArray: Observable<Array<ReplyModel>>
let outputMessage: Observable<String>
let isShowLoginAlert: Observable<Void>
}
var disposeBag = DisposeBag()
}
실제 ViewModel은 ViewModel 프로토콜을 채택하고 Input, Output, disposeBag을 정의해준다.
이 패턴의 가장 큰 지향점은 모든 사용자 이벤트를 ViewModel로 넘겨 비즈니스 로직을 ViewModel에서만 처리하도록 하는 것이기 때문에 버튼의 탭 이벤트, 텍스트필드의 입력 이벤트 등을 전부 Input에 정의하고 View로 넘겨줄 데이터들을 Output에 정의해주면 된다.
// MainChatViewModel.swift
private let replyArray = BehaviorRelay(value: Array<ReplyModel>())
private let inputMessage = BehaviorRelay(value: "")
private let isShowLoginAlert = PublishRelay<Void>()
func transform(input: Input) -> Output {
input.inputMessage.bind(to: inputMessage).disposed(by: disposeBag)
loadData()
input.tapSend.subscribe(
onNext: sendChat
).disposed(by: disposeBag)
input.tapChatTextField.bind(to: isShowLoginAlert).disposed(by: disposeBag)
return Output(replyArray: replyArray.asObservable(), outputMessage: inputMessage.asObservable(), isShowLoginAlert: isShowLoginAlert.asObservable())
}
그리고 transform에서는 input으로 받은 이벤트나 데이터 등을 처리해 Output으로 반환하도록 구현한다.
뷰모델 내에서 저장하거나 사용해야하는 데이터들은 뷰모델의 멤버 변수로 정의해 저장하거나 구독했다.
이 부분에서 굳이…? 싶었지만 안드로이드의 AAC에서 MutableLiveData와 LiveData를 각자 생성하는 것과 비슷하다고 생각하니 이해할 수 있었다.
// MainChatView.swift
private let viewModel = MainChatViewModel()
private func bindViewModel() {
let input = MainChatViewModel.Input(
inputMessage: textFieldReply.rx.text.orEmpty.asObservable(),
tapSend: buttonSend.rx.tap.asObservable(),
tapChatTextField: textFieldReply.rx.controlEvent(.touchDown).asObservable()
)
let output = viewModel.transform(input: input)
output.replyArray.bind(to: tableViewReply.rx.items(cellIdentifier: "ChatBubbleCell", cellType: ChatBubbleCell.self)) { (index: Int, element: ReplyModel, cell: ChatBubbleCell) in
cell.setContent(content: element.content)
}.disposed(by: disposeBag)
output.outputMessage.bind(to: textFieldReply.rx.text).disposed(by: disposeBag)
output.isShowLoginAlert.subscribe(
onNext: mainViewModel.showAlert
).disposed(by: disposeBag)
}
그리고 View의 bindViewModel 함수에서 Input 객체를 생성하고 transform 함수를 통해 Output 객체를 리턴받아 사용하면 된다.
처음 이 구조를 접하고 구현하고 시뮬레이터를 돌려볼 때 까지만 해도 굳이 이렇게까지 구현할 필요가 있을까? 했는데
View에서는 완전히 Snapkit으로 뷰를 구현하는 코드만 남고, 모든 처리가 ViewModel에 들어가니 코드 가독성이나 유지보수 측면에서 상당한 이득을 얻을 수 있었다.
참고자료
https://mrgamza.tistory.com/509
https://blog.gangnamunni.com/post/HealingPaperTV-ViewModel-Test/