[iOS] MVVM+Combine input/output Pattern

1 minute read

a ViewModel to use @Published of combine for binding data

class ObservableViewModel: ObservableObject {
    @Published private(set) var models: [Model]?
    private var bag: Set<AnyCancellable> = Set<AnyCancellable>()
    
    func fetch() {
        DataSource
            .shared
            .get()
            .sink { completion in
                if case .failure(let error) = completion {
                    print("Error : \(error)")
                }
            } receiveValue: { models in
                print("Results : \(models)")
                self.models = models
            }
            .store(in: &bag)
    }
}

input/output Pattern

ViewModel

protocol ViewModelType {
    associatedtype InputType
    associatedtype OutputType
    
    func trans(_ input: AnyPublisher<InputType, Never>) -> AnyPublisher<OutputType, Error>
}

enum Input {
    case fetch
}

enum Output {
    case loadData([Model])
}

class ViewModel: ViewModelType {
    private var bag: Set<AnyCancellable> = Set<AnyCancellable>()
    
    func trans(_ input: AnyPublisher<Input, Never>) -> AnyPublisher<Output, Error> {
        return input
            .flatMap({ input -> AnyPublisher<Output, Error> in
                switch input {
                case .fetch:
                    return DataSource
                        .shared
                        .get()
                        .map { models in
                            return .loadData(models)
                        }
                        .eraseToAnyPublisher()
                }
                
            })
            .eraseToAnyPublisher()
    }
}

Model & DataSource

struct Model {
    let title: String
}

final class DataSource {
    static let shared = DataSource()
    private init() {}
    
    func get() -> AnyPublisher<[Model], Error> {
        return Just(["title1", "title2"])
            .setFailureType(to: Error.self)
            .map({ $0.map({ Model(title: $0) }) })
            .eraseToAnyPublisher()
    }
}

View

class View: UIView {
    let viewModel: ViewModel = ViewModel()
    private var bag: Set<AnyCancellable> = Set<AnyCancellable>()
    private let input: PassthroughSubject<ViewModel.InputType, Never> = .init()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        bind()
        input.send(.fetch)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func bind() {
        viewModel
            .trans(input.eraseToAnyPublisher())
            .sink { completion in
                if case .failure(let error) = completion {
                    print("Error : \(error)")
                }
            } receiveValue: { output in
                if case .loadData(let models) = output {
                    print("Results : \(models)")
                }
            }
            .store(in: &bag)
    }
}

Reference

Leave a comment