Keeping Track of Text Changes over Two Text Fields

I'm still a beginner in using Combine. I practice it on and off. Anyway, I have a view model to see changes in two text fields in my view controller as follows.

// ViewModel //
import Foundation
import Combine

class LoginViewModel {
	var cancellable = [AnyCancellable]()
	
	
	init(username: String, password: String) {
		myUsername = username
		myPassword = password
	}
	
	@Published var myUsername: String?
	@Published var myPassword: String?
	
	func validateUser() {
		print("\(myUsername)")
		print("\(myPassword)")
	}
}

And my view controller goes as follows.

// ViewController //
import UIKit
import Combine

class HomeViewController: UIViewController {
	// MARK: - Variables
	var cancellable: AnyCancellable?
	
	
	// MARK: - IBOutlet
	@IBOutlet var usernameTextField: UITextField!
	@IBOutlet var passwordTextField: UITextField!
	
	
	// MARK: - Life cycle
	override func viewDidLoad() {
		super.viewDidLoad()
		
		cancellable = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameTextField)
		 .sink(receiveValue: { result in
			 if let textField = result.object as? UITextField {
				 if let text = textField.text {
					 let loginViewModel = LoginViewModel(username: text, password: "")
					 loginViewModel.validateUser()
				 }
			 }
		 })
	}
}

So I use NSNotification as a publisher to see text changes over one of the text fields. And I cannot see text changes over two of them at the same time. Is there a better approach in seeing text changes over two text fields at the same time using Combine?

Muchos thankos.

Answered by OOPer in 693346022

I'm still a beginner in using Combine.

Is there a better approach in seeing text changes over two text fields at the same time using Combine?

Unfortunately, it is hard to find what would be the best practice when using Combine in UIKit apps, as, you may know, there are no simple ways to integrate Combine and UIKit. So, every developer is a beginner.

But one thing critically bad in your example is that you create a new instance of LoginViewModel at each time Notification is received.


Other than that, I cannot say which is the better as there are very few examples on the web.

The following is just that I would write something like this in cases you described:

ViewModel

import UIKit
import Combine

class LoginViewModel {
    //↓Use _plural_ form
    var cancellables = Set<AnyCancellable>()
    
    init() {
        $myUsername
            .removeDuplicates()
            .combineLatest($myPassword.removeDuplicates())
            .sink(receiveValue: {username, password in
                self.validateUser(username: username, password: password)
            })
            .store(in: &cancellables)
    }
    
    convenience init(username: String, password: String) {
        self.init()
        
        myUsername = username
        myPassword = password
    }
    
    @Published var myUsername: String?
    @Published var myPassword: String?
    
    func validateUser(username: String?, password: String?) {
        print("\(username ?? "")")
        print("\(password ?? "")")
        //If you need to update some UI, add another publisher to which the ViewController can subscribe to.
        //...
    }
}

import UIKit

extension LoginViewModel {
    
    func bind(_ textField: UITextField, to property: ReferenceWritableKeyPath<LoginViewModel, String?>) {
        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: textField)
                 .sink(receiveValue: { result in
                     if let textField = result.object as? UITextField {
                         self[keyPath: property] = textField.text
                     }
                 })
                 .store(in: &cancellables)
    }
}

ViewController

import UIKit
import Combine

class HomeViewController: UIViewController {
    // MARK: - Variables
    let viewModel = LoginViewModel()
    
    // MARK: - IBOutlet
    @IBOutlet var usernameTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!
    
    
    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.bind(usernameTextField, to: \.myUsername)
        viewModel.bind(passwordTextField, to: \.myPassword)
    }
}
Accepted Answer

I'm still a beginner in using Combine.

Is there a better approach in seeing text changes over two text fields at the same time using Combine?

Unfortunately, it is hard to find what would be the best practice when using Combine in UIKit apps, as, you may know, there are no simple ways to integrate Combine and UIKit. So, every developer is a beginner.

But one thing critically bad in your example is that you create a new instance of LoginViewModel at each time Notification is received.


Other than that, I cannot say which is the better as there are very few examples on the web.

The following is just that I would write something like this in cases you described:

ViewModel

import UIKit
import Combine

class LoginViewModel {
    //↓Use _plural_ form
    var cancellables = Set<AnyCancellable>()
    
    init() {
        $myUsername
            .removeDuplicates()
            .combineLatest($myPassword.removeDuplicates())
            .sink(receiveValue: {username, password in
                self.validateUser(username: username, password: password)
            })
            .store(in: &cancellables)
    }
    
    convenience init(username: String, password: String) {
        self.init()
        
        myUsername = username
        myPassword = password
    }
    
    @Published var myUsername: String?
    @Published var myPassword: String?
    
    func validateUser(username: String?, password: String?) {
        print("\(username ?? "")")
        print("\(password ?? "")")
        //If you need to update some UI, add another publisher to which the ViewController can subscribe to.
        //...
    }
}

import UIKit

extension LoginViewModel {
    
    func bind(_ textField: UITextField, to property: ReferenceWritableKeyPath<LoginViewModel, String?>) {
        NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: textField)
                 .sink(receiveValue: { result in
                     if let textField = result.object as? UITextField {
                         self[keyPath: property] = textField.text
                     }
                 })
                 .store(in: &cancellables)
    }
}

ViewController

import UIKit
import Combine

class HomeViewController: UIViewController {
    // MARK: - Variables
    let viewModel = LoginViewModel()
    
    // MARK: - IBOutlet
    @IBOutlet var usernameTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!
    
    
    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.bind(usernameTextField, to: \.myUsername)
        viewModel.bind(passwordTextField, to: \.myPassword)
    }
}

May I ask how I can receive a bool result in my view controller like

func validateUser(username: String?, password: String?) -> AnyPublisher<Bool, Never> {
	if let username = username, let password = password {
			if !username.isEmpty && !password.isEmpty {
				// true
			}
	}
}

, please?

May I ask how I can receive a bool result in my view controller like

Generally, you should not include two or more topics in one thread..., though I'm not sure if this would be a part of the first topic or not.


I would do that as follows:

ViewModel

    @Published var isUserValid: Bool = false
    
    func validateUser(username: String?, password: String?) {
        if let username = username, !username.isEmpty,
           let password = password, !password.isEmpty {
            isUserValid = true
        } else {
            isUserValid = false
        }
    }

ViewController

    private var cancellables = Set<AnyCancellable>()
    // MARK: - Life cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        viewModel.bind(usernameTextField, to: \.myUsername)
        viewModel.bind(passwordTextField, to: \.myPassword)
        
        viewModel.$isUserValid
            .removeDuplicates()
            .sink {isUserValid in
                print(isUserValid)
                //...
            }
            .store(in: &cancellables)
    }
Keeping Track of Text Changes over Two Text Fields
 
 
Q