iOS/RxSwift

MVVM) Input-Output 패턴 적용하기 (feat. RxSwift)

Dev.Andy 2023. 11. 8. 03:06

머리말

요약

클릭하면 더 자세히 볼 수 있습니다

수업 시간에 배운 내용의 흐름을 위와 같이 정리해 보았다

(클릭하면 더 자세히 볼 수 있습니다)

본문

종류와 역할 및 특징

종류   역할 및 특징
View / ViewController 1. UI를 구성
2. 이벤트를 감지하여 ViewModel에 입력값으로 전달
3. ViewModel이 전달한 출력값을 화면에 띄움
Input View/ViewController의  이벤트를 감지하여 ViewModel에 보낼 데이터
ViewModel 1. UI 로직과 비즈니스 로직의 분리
2. MVC 패턴에서 과도한 기능을 분리
Output ViewModel에서 가공하여 View/ViewController에 표현할 데이터
bind 1. View/ViewController의 클래스 메서드
2. 나머지 요소 (View / ViewModel / Input / Output)를 서로 연결

코드

1) View / ViewController

import UIKit

import RxSwift
import RxCocoa

final class ValidateViewController: UIViewController {
    
    @IBOutlet private var nameTextField: UITextField!
    @IBOutlet private var validationLabel: UILabel!
    @IBOutlet private var nextButton: UIButton!
    
    private let viewModel = ValidateViewModel()
    
    private let disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        bind()
    }
    
    private func bind() {
        //...
    }
}

2) Input

2-1) 구조체 Input은 클래스인 ViewModel에 있다

class ValidateViewModel {
    struct Input {
        let text: ControlProperty<String?> // nameTextField.rx.text
        let tap: ControlEvent<Void> // nextButton.rx.tap
    }
}

2-2) 구조체 인스턴스 input은 View/ViewController의 메서드 bind()에 있다

final class ValidateViewController: UIViewController {
    private func bind() {
        let input = ValidateViewModel.Input(text: nameTextField.rx.text, tap: nextButton.rx.tap)
    }
}

3) ViewModel

class ValidateViewModel {
	
    struct Input {
        let text: ControlProperty<String?>
        let tap: ControlEvent<Void>
    }
    
    struct Output {
        let text: Driver<String>
        let tap: ControlEvent<Void>
        let validation: Observable<Bool>
    }

    func transform(input: Input) -> Output {
        let validation = input.text
            .orEmpty
            .map { $0.count >= 8 }
        
        let validText = BehaviorRelay(value: "닉네임은 8자 이상입니다")
            .asDriver()
        
        return Output(
            text: validText,
            tap: input.tap,
            validation: validation
        )
    }
}

4) Output

4-1) 구조체 Output은 클래스인 ViewModel에 있다

class ValidateViewModel {    
    struct Output {
        let text: Driver<String>
        let tap: ControlEvent<Void>
        let validation: Observable<Bool>
    }
}

4-2) 구조체 인스턴스 output은 View/ViewController의 메서드 bind()에 있다

final class ValidateViewController: UIViewController {
    private func bind() {
        let output = viewModel.transform(input: input)
    }
}

5) bind

final class ValidateViewController: UIViewController {
    private func bind() {
        
        let input = ValidateViewModel.Input(text: nameTextField.rx.text, tap: nextButton.rx.tap)
        let output = viewModel.transform(input: input)
        
        output.text
            .drive(validationLabel.rx.text)
            .disposed(by: disposeBag)
        
        output.validation
            .bind(to: nextButton.rx.isEnabled, validationLabel.rx.isHidden)
            .disposed(by: disposeBag)
        
        output.validation
            .bind(with: self) { owner, value in
                let color: UIColor = value ? .systemRed : .lightGray
                owner.nextButton.backgroundColor = color
            }
            .disposed(by: disposeBag)
        
        output.tap
            .bind(with: self) { owner, _ in
                print("nextButton CLICKED")
            }
            .disposed(by: disposeBag)
    }
}

꼬리말

MVVM에 정답은 없다

위와 같은 코드나 흐름이 무조건 정답은 아니다.

관점에 따라 다른 코드

어디까지를 Input으로 보고 어디까지를 Output로 봐야 하는지는 사람마다, 코드마다 다르다.

연습만이 살 길

따라서 무작정 하나의 코드를 고집하는 것이 아닌 여러 코드를 구현해 가면서 자신에게 맞는 로직과 주관을 갖는 게 중요하다 :)

코드로 실습하기

블로그 링크

LSLP) MVVM과 RxSwift를 이용한 반응형 이메일 입력 화면 구현 (feat: Input-Output & BehaviorRelay)

(2023-11-15 추가)