Search code examples
iosswiftmvvmrx-swift

Passed in data MVVM RxSwift


I want to learn MVVM RxSwift with input and output method, I want to get a username from textfield.

I have a scenario when user not enter a username it will present an error and when user enter a username it will present in viewController.

This is when I confuse. I got the error message and successfully present error but, how can I catch the query in my viewModel and passed the data to viewController.

This is how I setup my searchViewModel

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

class SearchViewModel: ViewModelType {
                
    // MARK: Binding
    struct Input {
        let searchText: Observable<String>
        let validate: Observable<Void>
    }
    
    struct Output {
        let username: Driver<String>
    }
    
    func transform(input: Input) -> Output {
        let username = input.validate
            .withLatestFrom(input.searchText)
            .map { query in
                if query.isEmpty {
                    return "Please enter a username. We need to know who to look for"
                } else {
                    return query
                }
            }.asDriver(onErrorJustReturn: "")
        
        
        
        return Output(username: username)
    }
}

and this is my viewDidLoad in SearchViewController

    let searchTextField = GFTextField()
    let calloutBtn      = GFButton(backgroundColor: .systemGreen, title: "Get followers")
    
    private let disposeBag = DisposeBag()

override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .systemBackground
        setupImageView()
        setupTextfield()
        setupCalloutBtn()
        
        let input = SearchViewModel.Input(
            searchText: searchTextField.rx.text.orEmpty.asObservable(),
            validate: calloutBtn.rx.tap.asObservable())
        
        let output = viewModel.transform(input: input)
        
        output.username.drive { [weak self] username in
            guard let self = self else { return }
            self.presentGFAlertOnMainThread(title: "Empty Username", message: username, buttonTitle: "Dismiss")
        }.disposed(by: disposeBag)

    }

Solution

  • It depends on what you want to do with the text of course, but below I assume you want to make a network request. But of course that requires understanding what your API layer looks like. I have to make some assumptions there as well, but the key is that you need to inject your API layer into your view model through its constructor/init method.

    Like this:

    class SearchViewModel: ViewModelType {
    
        struct Input {
            let username: Observable<String>
            let getFollowers: Observable<Void>
        }
    
        struct Output {
            let errorMessage: Driver<String>
            let followers: Driver<[Follower]>
        }
    
        let api: API
    
        init(api: API) {
            self.api = api
        }
    
        func transform(input: Input) -> Output {
            let followersResponse = input.getFollowers
                .withLatestFrom(input.username)
                .filter { !$0.isEmpty }
                .map { makeEndpoint(using: $0) }
                .flatMap { [api] in
                    api.load($0)
                }
                .share()
    
            let missingName = input.getFollowers
                .withLatestFrom(input.username)
                .compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
    
            let errorMessage = Observable.merge(
                api.error.map { $0.localizedDescription },
                missingName
            )
            .asDriver(onErrorJustReturn: "")
    
            let followers = followersResponse
                .asDriver(onErrorJustReturn: [])
    
            return Output(errorMessage: errorMessage, followers: followers)
        }
    }
    

    -- EDIT --

    If all you want to do is push the non-empty text field back to the view controller, then it would look like this:

    class SearchViewModel: ViewModelType {
    
        struct Input {
            let username: Observable<String>
            let getFollowers: Observable<Void>
        }
    
        struct Output {
            let errorMessage: Driver<String>
            let username: Driver<String>
        }
    
        func transform(input: Input) -> Output {
    
            let errorMessage = input.getFollowers
                .withLatestFrom(input.username)
                .compactMap { $0.isEmpty ? "Please enter a username. We need to know who to look for" : nil }
                .asDriver(onErrorJustReturn: "")
    
            let username = input.getFollowers
                .withLatestFrom(input.username)
                .filter { !$0.isEmpty }
                .asDriver(onErrorJustReturn: "")
    
            return Output(errorMessage: errorMessage, username: username)
        }
    }
    

    The key here is that you need a Driver for each output that the view controller will want to subscribe to.