I know, this question is probably off topic for this forum and I am sorry, but I have no idea where else to ask it (if you have a better idea, please let me know).
Having watched "Combine in Practice" ( https://developer.apple.com/videos/play/wwdc2019/721/), especially the second part (starting at 22:50) I very much liked the username/password validation using Combine and UIKit.
Now I wonder how to replicate something similar using Combine and SwiftUI. I tried to put two Property Wrappers at once onto my properties. However, my naive approach did not work out:
import SwiftUI
import Combine
struct RegistrationView : View {
@Published @State private var eMail: String = ""
var body: some View {
Form {
TextField("Enter your EMail", text: $$eMail.value)
}
}
}
While I somehow get access to the inner Property Wrapper this way, it doesn't work as such or at least as I expected, I get the error message:
Cannot convert value of type 'State<String>' to expected argument type 'Binding<String>'
Is there a way to fix this and get something like this working (maybe I am overlooking something obvious) or will Combine and SwiftUI just not play well together?
Could you tell the purpose of @Published property in your case?
I suppose that you should "combine" signals from different views ( for example ) in your model, not in view.
@State is already a kind of "published property" - it is a binding property with "onChange" redrawing.
class RegistrationModel : BindableObject {
var username: String = ""
var password: String = ""
var email: String = ""
var didChange: PassthroughObject<Void, Never>
init() {
}
}
// MARK: TopView
struct TopRegistrationView {
@State var model: RegistrationModel
var body: some View {
UsernameView($model.username)
PasswordView($model.password)
RegistrationView($model.email)
AcceptButtonView(model.canAcceptData)
}
}
As you can see, model also have canAcceptData - it is a property which indicates button availability.
You can calculate this property canAcceptData on-fly, because redrawing cycle will lift it to a view for you.
Please, check my attempt to demonstrate different approaches in SwiftUI.
Thanks for your inspiration @lolgear! As you suggested I moved the @States into a model class and got it working this way. My code looks now like this, it is still work in progress and somewhat simpler than the code from your GitHub example. I post it here so others with similar problems get an idea how to get things working:
Update: I updated the code, got the password validation working (there were subtle mistakes in the code presented at the WWDC session (missing Publishers. before CombineLatest and missing .map after CombineLatest) but now the third publisher validatedCredentials never fires. I did set breakpoints in the lines with the guard in both validatedPassword and validatedCredentials , the one in validatedPassword was reached just fine but the one in validatedCredentials was never reached.
@IOOI well in swiftUI don't we have something called EnviromentObject which in theory is supposed to take care of our actual logic? as the following code suggests having a viewModel that contains all the actual logic, and separates it from our view. Correct me if I am wrong,
class ViewModel: BindableObject {
var didChange = PassthroughSubject<ViewModel, Never>()
var validated: String? {
didSet {
didChange.send(self)
}
}
@Published var passwordPublisher = ""
@Published var passwordConfirmationPublisher = ""
var validatedCredentials: AnyPublisher<String?, Never> {
return Publishers.CombineLatest($passwordPublisher, $passwordConfirmationPublisher)
.map { arg in
let (password, confirmPassword) = arg
guard password == confirmPassword, password.count > 8 else { return nil }
return password
}.eraseToAnyPublisher()
}
init() {
// bind the validated password to the validated field that we have
validatedCredentials.receive(on: RunLoop.main).assign(to: \.validated, on: self)
}
func handleSignin() {
// do something
}
}
struct ContentView: View {
@EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack {
TextField($viewModel.passwordPublisher, placeholder: Text("First password"))
TextField($viewModel.passwordConfirmationPublisher, placeholder: Text("First password"))
Button("Sign in") {
self.viewModel.handleSignin()
}.disabled(viewModel.validated == nil)
}
}
}
Hi,
Glad could help. Like to thank you for showing how to use .onReceive in SwiftUI & updating @State property also function usernameAvailable with completion called from Future publisher etc.. very compact & precise codes.