Input, Output 패턴 (with Rx, Combine)

godo·2022년 11월 20일
1

Swift - Design Patterns

목록 보기
24/24

설명

Controller 에서 Inputs 을 받아서 ViewModel 에서 이 Inputs 값을 바탕으로 처리를 하여 Controller 에 Outputs 해주는 방식입니다.

Rx

기본적인 프로토콜

protocol ViewModelType {
    associatedtype Input
    associatedtype Output
    
    func transform(input: Input) -> Output
}

subject 없는 버전

  • ViewModel
class ViewModel: ViewModelType {
    struct Input {
        let name: Observable<String>
        let validate: Observable<Void>
    }

    struct Output {
        let greeting: Driver<String>
    }


    func transform(input: Input) -> Output {
        let greeting = input.validate
            .withLatestFrom(input.name)
            .map { name in
                return "hello \(name)!"
            }
            .startWith("")
            .asDriver(onErrorJustReturn: ":-(")

        return Output(greeting: greeting)
    }

}
  • ViewController
class ViewController: UIViewController {

    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var greetingLabel: UILabel!
    @IBOutlet weak var validateButton: UIButton!
    
    private let viewModel = ViewModel()
    private var bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bind()
    }
    
    private func bind() {
        
        let inputs = ViewModel.Input(
            name: nameTextField.rx.text.orEmpty.asObservable(),
            validate: validateButton.rx.tap.asObservable()
        )
        
        let outputs = viewModel.transform(input: inputs)
        outputs.greeting
            .drive(greetingLabel.rx.text)
            .disposed(by: bag)
    }

}

subject 있는 버전

  • ViewModel
class ViewModel {
    let input: Input
    let output: Output

    struct Input {
        let name: AnyObserver<String>
        let validate: AnyObserver<Void>
    }

    struct Output {
        let greeting: Driver<String>
    }

    private let nameSubject = ReplaySubject<String>.create(bufferSize: 1)
    private let validateSubject = PublishSubject<Void>()

    init() {
        let greeting = validateSubject
            .withLatestFrom(nameSubject)
            .map { name in
                return "Hello \(name)!"
            }
            .asDriver(onErrorJustReturn: ":-(")

        self.output = Output(greeting: greeting)
        self.input = Input(name: nameSubject.asObserver(),
                           validate: validateSubject.asObserver())
    }
}
  • ViewController
class ViewController: UIViewController {

    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var greetingLabel: UILabel!
    @IBOutlet weak var validateButton: UIButton!
    
    private let viewModel = ViewModel()
    private var bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bind()
    }
    
    private func bind() {
        
        nameTextField.rx
            .text
            .orEmpty
            .bind(to: viewModel.input.name)
            .disposed(by: bag)
        
        validateButton.rx
            .tap
            .bind(to: viewModel.input.validate)
            .disposed(by: bag)
        
        viewModel.output.greeting
            .drive(greetingLabel.rx.text)
            .disposed(by: bag)
        
    }

}

Combine

  • ViewModel
class ViewModel {
    enum Input {
        case TextInput(String)
    }
    
    enum Output {
        case validate(String)
    }
    
    private let outPut: PassthroughSubject<Output, Never> = .init()
    private var cancellables = Set<AnyCancellable>()
    
    func transform(input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Never> {
        input.sink { [weak self] event in
            switch event {
            case .TextInput(let text):
                self?.outPut.send(.validate(text))
            }
        }
        .store(in: &cancellables)
        
        return outPut.eraseToAnyPublisher()
    }

}
  • ViewController
class ViewController: UIViewController {

    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var greetingLabel: UILabel!
    @IBOutlet weak var validateButton: UIButton!
    
    private let viewModel = ViewModel()
    private var cancellables = Set<AnyCancellable>()
    private let input: PassthroughSubject<ViewModel.Input, Never> = .init()

    override func viewDidLoad() {
        super.viewDidLoad()
        bind()
        setupButton()
    }
    
    private func bind() {
     
        let output = viewModel.transform(input: input.eraseToAnyPublisher())
        output.receive(on: DispatchQueue.main)
            .sink { [weak self] event in
                switch event {
                case .validate(let text):
                    self?.greetingLabel.text = "Hello \(text)"
                }
            }
            .store(in: &cancellables)
    
    }
    
    private func setupButton() {
        validateButton.addAction(UIAction(handler: { _ in
            self.input.send(.TextInput(self.nameTextField.text ?? ""))
        }), for: .touchUpInside)
    }

}

결과물

Reference

https://medium.com/blablacar/rxswift-mvvm-66827b8b3f10
https://www.youtube.com/watch?v=KK6ryBmTKHg&t=1880s

profile
☀️☀️☀️

0개의 댓글