Publishers.CombineLatest in SwiftUI

I've been using Combine with UIKit and Cocoa. The following is a simple example.

import UIKit
import Combine

class ViewController: UIViewController {
	// MARK: - Variables
	private var cancellableSet: Set<AnyCancellable> = []
	@Published var loginText: String = ""
	@Published var passwordText: String = ""
	
	
	// MARK: - IBOutlet
	@IBOutlet weak var loginField: UITextField!
	@IBOutlet weak var passwordField: UITextField!
	
	
	// MARK: - Life cycle
	override func viewDidLoad() {
		super.viewDidLoad()
		
		NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: loginField)
			.sink { result in
				if let textField = result.object as? UITextField {
					if let text = textField.text {
						self.loginText = text
					}
				}
			}
			.store(in: &cancellableSet)
		
		NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordField)
			.sink { result in
				if let textField = result.object as? UITextField {
					if let text = textField.text {
						self.passwordText = text
					}
				}
			}
			.store(in: &cancellableSet)
		
		Publishers.CombineLatest($loginText, $passwordText)
			.sink { (result0, result1) in
				if result0.count > 3 && result1.count > 3 {
					print("You are good")
				} else {
					print("No way!!!")
				}
			}
			.store(in: &cancellableSet)
	}
}

Now, I want to use Combine with SwiftUI. The following is SwiftUI equivalent, so far.

import SwiftUI
import Combine

struct ContentView: View {
	@State var anycancellables = Set<AnyCancellable>()
	@State var userText: String = ""
	@State var passText: String = ""
	@State var canSave: Bool = false
	
	var body: some View {
		ZStack {
			VStack {
				Color.white
			}.onTapGesture {
				UIApplication.shared.endEditing()
			}
			
			VStack {
				TextField("Username", text: $userText) {
					
				}.onChange(of: userText) { newValue in
					
				}
				SecureField("Password", text: $passText) {
					
				}.onChange(of: passText) { newValue in
					
				}
					
				Spacer()
					.frame(height: 20.0)
				Button("Save") {
					print("Saved...")
				}
				.foregroundColor(canSave ? Color.black : Color.gray)
				.font(.system(size: 32.0))
				.disabled(!canSave)
			}.padding(.horizontal, 20.0)
		}
	}
}

So where does Combine fit into the code? I want to enable the Save button if text counts of loginText and passwordText are both greater than 3, which is done at the top with UIKit.

Muchos thankos.

Answered by OOPer in 700025022

To utilize Combine well, you may need to know what are (or should be or can be) publisher. @State variables cannot be publishers without some additional code.

To make publishers easily, you can work with ObservableObject with @Published variables:

import SwiftUI
import Combine

class ValidateLogin {
    let user: String
    let pass: String
    init(user: String, pass: String) {
        self.user = user
        self.pass = pass
    }
    
    func validateMe() -> Bool {
        return user.count > 3 && pass.count > 3
    }
}

class MyContent: ObservableObject {
    @Published var userText: String = ""
    @Published var passText: String = ""
}

struct ContentView: View {
    @StateObject var content = MyContent()
    @State var canSave: Bool = false
    @State var contentSubscriber: AnyCancellable?
    
    var body: some View {
        ZStack {
            VStack {
                TextField("Username", text: $content.userText) {
                }
                SecureField("Password", text: $content.passText) {
                }
            }.padding(.horizontal, 20.0)
        }.onAppear {
            self.contentSubscriber = self.content.$userText
                .combineLatest(content.$passText)
                .sink {userText, passText in
                    let validateLogin = ValidateLogin(user: userText, pass: passText)
                    self.canSave = validateLogin.validateMe()
                }
        }
    }
}

I could do something like the following.

class ValidateLogin {
	var good: Bool = false
	let user: String
	let pass: String
	init(user: String, pass: String) {
		self.user = user
		self.pass = pass
	}
	func validateMe() -> Bool {
		if user.count > 3 && pass.count > 3 {
			good = true
		}
		return good
	}
}

struct ContentView: View {
	@State var userText: String = ""
	@State var passText: String = ""
	@State var canSave: Bool = false
	
	var body: some View {
		ZStack {
			VStack {
				TextField("Username", text: $userText) {
					
				}.onChange(of: userText) { newValue in
					let validateLogin = ValidateLogin(user: userText, pass: passText)
					canSave = validateLogin.validateMe()
				}
				SecureField("Password", text: $passText) {
					
				}.onChange(of: passText) { newValue in
					let validateLogin = ValidateLogin(user: userText, pass: passText)
					canSave = validateLogin.validateMe()
				}
			}.padding(.horizontal, 20.0)
		}.onAppear {
			//Publishers.CombineLatest($userText, $passText)
			
		}
	}
}

The code above doesn't involve Combine at all. I want to do it in a Combine way.

Accepted Answer

To utilize Combine well, you may need to know what are (or should be or can be) publisher. @State variables cannot be publishers without some additional code.

To make publishers easily, you can work with ObservableObject with @Published variables:

import SwiftUI
import Combine

class ValidateLogin {
    let user: String
    let pass: String
    init(user: String, pass: String) {
        self.user = user
        self.pass = pass
    }
    
    func validateMe() -> Bool {
        return user.count > 3 && pass.count > 3
    }
}

class MyContent: ObservableObject {
    @Published var userText: String = ""
    @Published var passText: String = ""
}

struct ContentView: View {
    @StateObject var content = MyContent()
    @State var canSave: Bool = false
    @State var contentSubscriber: AnyCancellable?
    
    var body: some View {
        ZStack {
            VStack {
                TextField("Username", text: $content.userText) {
                }
                SecureField("Password", text: $content.passText) {
                }
            }.padding(.horizontal, 20.0)
        }.onAppear {
            self.contentSubscriber = self.content.$userText
                .combineLatest(content.$passText)
                .sink {userText, passText in
                    let validateLogin = ValidateLogin(user: userText, pass: passText)
                    self.canSave = validateLogin.validateMe()
                }
        }
    }
}

I guess ObservableObject is a ticket to using Combine in SwiftUI. So I can write the following.

import SwiftUI
import Combine

struct ContentView: View {
	@State var cancellables = Set<AnyCancellable>()
	@StateObject var login = Login()
	@State var canSave: Bool = false
	
	var body: some View {
		VStack {
			Text("Login")
			TextField("Enter username", text: $login.user)
			TextField("Enter password", text: $login.pass)
			Button("Save") {
				
			}
			.foregroundColor(canSave ? Color.orange : Color.gray)
			.font(.system(size: 40.0))
			.disabled(!canSave)
		}
		.padding(.horizontal, 40.0)
		.onAppear {
			Publishers.CombineLatest(login.$user, login.$pass)
				.sink { completion in
					print(completion)
				} receiveValue: { (result0, result1) in
					let bool = (result0.count > 3 && result1.count > 3)
					canSave = bool
				}.store(in: &cancellables)
		}
	}
}

class Login: ObservableObject {
	@Published var user: String = ""
	@Published var pass: String = ""
}

This is really good stuff.

Publishers.CombineLatest in SwiftUI
 
 
Q