Thanks for your inspiration @lolgear! As you suggested I moved the @State
s 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:
//
// RegistrationView.swift
//
// Created by Lars Sonchocky-Helldorf on 04.07.19.
// Copyright © 2019 Lars Sonchocky-Helldorf. All rights reserved.
//
import SwiftUI
import Combine
struct RegistrationView : View {
@ObjectBinding var registrationModel = RegistrationModel()
@State private var showAlert = false
@State private var alertTitle: String = ""
@State private var alertMessage: String = ""
@State private var registrationButtonDisabled = true
@State private var validatedEMail: String = ""
@State private var validatedPassword: String = ""
var body: some View {
Form {
Section {
TextField("Enter your EMail", text: $registrationModel.eMail)
SecureField("Enter a Password", text: $registrationModel.password)
SecureField("Enter the Password again", text: $registrationModel.passwordRepeat)
Button(action: registrationButtonAction) {
Text("Create Account")
}
.disabled($registrationButtonDisabled.value)
.presentation($showAlert) {
Alert(title: Text("\(alertTitle)"), message: Text("\(alertMessage)"))
}
.onReceive(self.registrationModel.validatedCredentials) { newValidatedCredentials in
self.registrationButtonDisabled = (newValidatedCredentials == nil)
}
}
Section {
Text("Validated EMail: \(validatedEMail)")
.onReceive(self.registrationModel.validatedEMail) { newValidatedEMail in
self.validatedEMail = newValidatedEMail != nil ? newValidatedEMail! : "EMail invalid"
}
Text("Validated Password: \(validatedPassword)")
.onReceive(self.registrationModel.validatedPassword) { newValidatedPassword in
self.validatedPassword = newValidatedPassword != nil ? newValidatedPassword! : "Passwords to short or don't matchst"
}
}
}
.navigationBarTitle(Text("Sign Up"))
}
func registrationButtonAction() {
let trimmedEMail: String = self.registrationModel.eMail.trimmingCharacters(in: .whitespaces)
if (trimmedEMail != "" && self.registrationModel.password != "") {
NetworkManager.sharedInstance.registerUser(NetworkManager.RegisterRequest(uid: trimmedEMail, password: self.registrationModel.password)) { (status) in
if status == 200 {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration successful", comment: "")
self.alertMessage = NSLocalizedString("please verify your email and login", comment: "")
} else if status == 400 {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration Error", comment: "")
self.alertMessage = NSLocalizedString("already registered", comment: "")
} else {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration Error", comment: "")
self.alertMessage = NSLocalizedString("network or app error", comment: "")
}
}
} else {
self.showAlert = true
self.alertTitle = NSLocalizedString("Registration Error", comment: "")
self.alertMessage = NSLocalizedString("username / password empty", comment: "")
}
}
}
class RegistrationModel : BindableObject {
@Published var eMail: String = ""
@Published var password: String = ""
@Published var passwordRepeat: String = ""
public var didChange = PassthroughSubject<Void, Never>()
var validatedEMail: AnyPublisher<String?, Never> {
return $eMail
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { username in
return Future { promise in
self.usernameAvailable(username) { available in
promise(.success(available ? username : nil))
}
}
}
.eraseToAnyPublisher()
}
var validatedPassword: AnyPublisher<String?, Never> {
return Publishers.CombineLatest($password, $passwordRepeat)
.debounce(for: 0.5, scheduler: RunLoop.main)
.map { password, passwordRepeat in
guard password == passwordRepeat, password.count > 5 else { return nil }
return password
}
.eraseToAnyPublisher()
}
var validatedCredentials: AnyPublisher<(String, String)?, Never> {
return Publishers.CombineLatest(validatedEMail, validatedPassword)
.map { validatedEMail, validatedPassword in
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
return (eMail, password)
}
.eraseToAnyPublisher()
}
func usernameAvailable(_ username: String, completion: (Bool) -> Void) {
let isValidEMailAddress: Bool = NSPredicate(format:"SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}").evaluate(with: username)
completion(isValidEMailAddress)
}
}
#if DEBUG
struct RegistrationView_Previews : PreviewProvider {
static var previews: some View {
RegistrationView()
}
}
#endif
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.
What is going on here?