I'm playing around a tad with Combine and SwiftUI for a little pet project of mine, learning as I go.
Here's the LoginModel at it current state:
public class LoginModel: ObservableObject {
#Published var domain: String = ""
#Published var email: String = ""
#Published var password: String = ""
#Published var isValid: Bool = false
public var didChange = PassthroughSubject<Void, Never>()
var credentialsValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest($email, $password)
.receive(on: RunLoop.main)
.map { (email, password) in
let emailValid = String.emailValid(emailString: email) // String extension function
let passwordValid = password.count > 5
return emailValid && passwordValid
}
.breakpointOnError()
.eraseToAnyPublisher()
}
init() {
// This works just fine
_ = credentialsValidPublisher.sink { isValid in
self.isValid = isValid
}
// However this does not work at all
_ = domain
.publisher
.receive(on: RunLoop.main)
.sink { value in
print(value)
}
}
}
Now from my current understanding is that a #Published var foo: String already has a Publisher attached to it. And that one should be able to use this directly to subscribe to its changes.
Changing credentialsValidPublisher variable over to this works too:
var credentialsValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest3($domain, $email, $password)
.receive(on: RunLoop.main)
.map { (domain, email, password) in
let domainValid = URL.isValidURL(urlString: domain)
let emailValid = String.emailValid(emailString: email)
let passwordValid = password.count > 5
return domainValid && emailValid && passwordValid
}
.breakpointOnError()
.eraseToAnyPublisher()
}
But this is not what I want. In my case I need a special Publisher in order to map a valid URL string over to a network request and then ping the server at hand to see if the provided server is responding.
Also this model is then connected to a SwiftUI view with a bunch of SwiftUI TextFields.
Any help to point me in the right direction will be highly appreciated.
So I figure out the way to do it.
In the LoginModel underneath var credentialsValidPublisher: AnyPublisher<Bool, Never> I added:
var domainValidPublisher: AnyPublisher<Bool, Never> {
$domain
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.removeDuplicates()
.map { domain in
URL.isValidURL(urlString: domain)
}
.eraseToAnyPublisher()
}
Then I can just subscribe to it in init.
I also added AnyCancellable properties that we call .cancel() on deinit.
Here's what the updated model looks like:
public class LoginModel: ObservableObject {
#Published var domain: String = ""
#Published var email: String = ""
#Published var password: String = ""
#Published var isValid: Bool = false
public var didChange = PassthroughSubject<Void, Never>()
private var credentialsValidPublisherCancellable: AnyCancellable!
private var domainValidCancellable: AnyCancellable!
var credentialsValidPublisher: AnyPublisher<Bool, Never> {
Publishers.CombineLatest3($domain, $email, $password)
.receive(on: RunLoop.main)
.map { (domain, email, password) in
let domainValid = URL.isValidURL(urlString: domain)
let emailValid = String.emailValid(emailString: email)
let passwordValid = password.count > 5
return domainValid && emailValid && passwordValid
}
.breakpointOnError()
.eraseToAnyPublisher()
}
var domainValidPublisher: AnyPublisher<Bool, Never> {
$domain
.debounce(for: 0.5, scheduler: DispatchQueue.main)
.removeDuplicates()
.map { domain in
URL.isValidURL(urlString: domain)
}
.eraseToAnyPublisher()
}
init() {
credentialsValidPublisherCancellable = credentialsValidPublisher.sink { isValid in
self.isValid = isValid
}
domainValidCancellable = domainValidPublisher.sink { isValid in
print("isValid: \(isValid)")
}
}
deinit {
credentialsValidPublisherCancellable.cancel()
domainValidCancellable.cancel()
}
}
Related
When I compile I get this error "The path to the document cannot be empty".
To fix this, I should add an unescaped closure.
How to add an unescaped closure to the fetchUsers () function and call the GetCorsodiLaurea () function in the closure? In such a way, the compiler will not try to execute functions asynchronously.
LoginViewModel
import SwiftUI
import Firebase
import LocalAuthentication
class LoginViewModel: ObservableObject {
#Published var email: String = ""
#Published var password: String = ""
#AppStorage("use_face_id") var useFaceID: Bool = false
#AppStorage("use_face_email") var faceIDEmail: String = ""
#AppStorage("use_face_password") var faceIDPassword: String = ""
//Log Status
#AppStorage("log_status") var logStatus: Bool = false
//MARK: Error
#Published var showError: Bool = false
#Published var errorMsg: String = ""
// MARK: Firebase Login
func loginUser(useFaceID: Bool,email: String = "",password: String = "")async throws{
let _ = try await Auth.auth().signIn(withEmail: email != "" ? email : self.email, password: password != "" ? password : self.password)
DispatchQueue.main.async {
if useFaceID && self.faceIDEmail == ""{
self.useFaceID = useFaceID
// MARK: Storing for future face ID Login
self.faceIDEmail = self.email
self.faceIDPassword = self.password
}
self.logStatus = true
}
}
//MARK: FaceID Usage
func getBioMetricStatus()->Bool{
let scanner = LAContext()
return scanner.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: .none)
}
// MARK: FaceID Login
func autenticationUser()async throws{
let status = try await LAContext().evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "To Login Into App")
if status{
try await loginUser(useFaceID: useFaceID,email: self.faceIDEmail,password: self.faceIDPassword)
}
}
}
ProfileViewModel
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
import SwiftUI
class ProfileViewModel: ObservableObject {
#Published var userInfo: UserModel = .empty
#Published var userDegree: userDegreeModel = .empty
#Published var isSignedIn = false
#Published var showError: Bool = false
#Published var errorMsg: String = ""
var uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.uid = user.uid
self.isSignedIn = true
}
else {
self.isSignedIn = false
self.userInfo.Nomeintero = ""
}
}
fetchUser() { [self] in
fetchDegrees()
}
}
func fetchUser(completion: #escaping () -> Void) {
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMsg = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userInfo = try document.data(as: UserModel.self)!
completion()
}
catch {
print(error)
}
}
}
}
}
func fetchDegrees() {
let docRef = db.collection("DegreeCourses").document(userInfo.Tipocorso)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMsg = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userDegree = try document.data(as: userDegreeModel.self)!
}
catch {
print(error)
}
}
}
}
}
}
UserModel
import SwiftUI
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
public struct UserModel: Codable{
#DocumentID var id: String?
var Nome : String
var Cognome : String
var photoURL : String
var Nomeintero : String
var Corsodilaurea : String
var Tipocorso : String
}
extension UserModel {
static let empty = UserModel(Nome: "", Cognome: "", photoURL: "", Nomeintero: "", Corsodilaurea: "", Tipocorso: "")
}
userDegreeModel
import SwiftUI
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
struct userDegreeModel: Codable {
#DocumentID var id: String?
var Name : String
var TotalSubjects : Int
}
extension userDegreeModel {
static let empty = userDegreeModel(Name: "", TotalSubjects: 0)
}
Error
A couple of notes:
Firestore calls return on the main dispatch queue already about this), so you don't need to manually switch to the main queue using DispatchQueue.async { }. See my Twitter thread for more details.
Instead of mapping Firestore documents manually, you can use Codable to do so. This means less code to write, and fewer typos :-) Here is an article that goes into much more detail: Mapping Firestore Data in Swift - The Comprehensive Guide | Peter Friese
Accessing the signed in user using Auth.auth().currentUser!.uid might result in you app breaking if no user is signed in. I recommend implementing an authentication state listener instead.
Since all of Firebase's APIs are asynchronous (see my blog post about this: Calling asynchronous Firebase APIs from Swift - Callbacks, Combine, and async/await | Peter Friese), the result of fetchUser will take a short moment, so you want to make sure to only call fetchDegrees once that call has completed. One way to do this is to use a completion handler.
Lastly, I recommend following a styleguide like this one for naming your classes and attribute: Swift Style Guide
I've updated your code accordingly below.
import Firebase
import FirebaseDatabase
import FirebaseFirestoreSwift
import SwiftUI
public struct UserModel: Codable {
#DocumentID var id: String?
var firstName: String
var lastName: String
var photoUrl: String
// ...
}
extension UserModel {
static let empty = UserModel(firstName: "", lastName: "", photoUrl: "")
}
public struct UserDegreeModel: Codable {
#DocumentID var id: String?
var name: String
var totalSubjects: Int
// ...
}
extension UserDegreeModel {
static let empty = UserDegreeModel(name: "", totalSubjects: 0)
}
class ProfileViewModel: ObservableObject {
#Published var userInfo: UserModel = .empty
#Published var userDegree: UserDegreeModel = .empty
#Published var isSignedIn = false
let uid = Auth.auth().currentUser!.uid
let db = Firestore.firestore()
init() {
// listen for auth state change and set isSignedIn property accordingly
Auth.auth().addStateDidChangeListener { auth, user in
if let user = user {
print("Signed in as user \(user.uid).")
self.uid = user.uid
self.isSignedIn = true
}
else {
self.isSignedIn = false
self.username = ""
}
}
fetchUser() {
fetchDegrees()
}
}
func fetchUser(completion: () -> Void) {
let docRef = db.collection("users").document(uid)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.user = try document.data(as: UserModel.self)
completion()
}
catch {
print(error)
}
}
}
}
}
func fetchDegrees() {
let docRef = db.collection("DegreeCourses").document(userInfo.Tipocorso)
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.userDegree = try document.data(as: UserDegreeModel.self)
}
catch {
print(error)
}
}
}
}
}
}
I want to observe changes on UISearchController's text, here is my setup (and I'm pretty new to Combine):
private var searchQuery: String? {
didSet {
print(searchQuery)
}
}
private var disposable: AnyCancellable?
func bindSearchQuery() {
disposable = searchController.searchBar.publisher(for: \.text)
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { value in
if let _value = value {
self.searchQuery = _value
}
}
}
And I see this once only in the console, on load:
Optional("")
I had to create an AnyPublisher object then received its changes on an AnyCancellable object:
final class SearchResult {
var resultText: String? {
didSet {
print(resultText)
}
}
}
///
#Published var searchQuery: String = ""
private var validateQuery: AnyPublisher<String?, Never> {
return $searchQuery
.debounce(for: 0.3, scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { query in
return Future { promise in
self.search(query: query) { result in
switch result {
case .success(let _output):
print(_output)
promise(.success(_output))
case .failure:
print("Failed search")
promise(.success(nil))
}
}
}
}
.eraseToAnyPublisher()
}
private var cancelable: AnyCancellable?
private var result = SearchResult()
///
func bindSearchQuery() {
cancelable = validateQuery
.receive(on: RunLoop.main)
.assign(to: \.resultText, on: result)
}
And everytime user types in the search box I update searchQuery:
func updateSearchResults(for searchController: UISearchController) {
self.searchQuery = searchController.searchBar.text ?? ""
}
I am trying to figure out why my session is not updating despite didSet firing and presumably updating User.
I've removed superfluous comments and style from the snippets but this is what I am trying currently.
// SessionStore.swift
class User {
var uid: String
var phoneNumber: String
init(uid: String, phoneNumber: String) {
self.uid = uid
self.phoneNumber = phoneNumber
}
}
class SessionStore : ObservableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? { didSet { self.didChange.send(self); print("didSet()") }}
var handle: AuthStateDidChangeListenerHandle?
func listen () {
// monitor authentication changes using firebase
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
self.session = User(
uid: user.uid,
phoneNumber: ""
)
} else {
self.session = nil
}
}
}
func register(
phoneNumber: String,
handler: #escaping AuthDataResultCallback
){
PhoneAuthProvider.provider(auth: Auth.auth())
PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in
if let error = error {
return
}
UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
let verificationID = UserDefaults.standard.string(forKey: "authVerificationID")
let verificationCode = "123456"
let credential = PhoneAuthProvider.provider().credential(
withVerificationID: verificationID!,
verificationCode: verificationCode
)
Auth.auth().signIn(with: credential) { (authResult, error) in
if let error = error {
print(error)
return
}
self.listen()
return
}
}
}
}
and
struct Login: View {
#EnvironmentObject var session: SessionStore
#State var phoneNumber: String = ""
#State var loading = false
#State var error = false
func getUser () {
session.listen()
}
var body: some View {
Group {
// If the user is logged in
if(session.session != nil) {
Score()
// If the user is NOT logged in
} else {
VStack {
Text("Register!")
TextField("Phone Number", text: $phoneNumber)
MyButton(...).gesture(TapGesture().onEnded {
self.session.register(
phoneNumber: self.phoneNumber
){
(result, error) in
if(error != nil) {
self.error = true
} else {
self.phoneNumber = ""
self.getUser()
}
})
}
}
}
}.onAppear(perform: getUser)
}
}
Scratchy (only based on your snapshot)...
Instead of this
class SessionStore : ObservableObject {
var didChange = PassthroughSubject<SessionStore, Never>()
var session: User? { didSet { self.didChange.send(self); print("didSet()") }}
use this
class SessionStore : ObservableObject {
#Published var session: User?
as far as I see from snapshot no more changes should be needed, might be some type alignment in other parts.
I have written a custom PropertyWrapper, that tries to wrap UserDefaults while also giving them the same behaviour as a #Published variable. It almost works, except that the ObservableObject does not propagate the changes without observing the UserDefaults themselves.
I cannot pass a objectWillChange ref to the #Setting init, as self is not available during Settings.init...
I wonder how #Published does that..
import Combine
import Foundation
class Settings: ObservableObject {
// Trying to avoid this:
/////////////////////////////////////////////
let objectWillChange = PassthroughSubject<Void, Never>()
private var didChangeCancellable: AnyCancellable?
private init(){
didChangeCancellable = NotificationCenter.default
.publisher(for: UserDefaults.didChangeNotification)
.map { _ in () }
.receive(on: DispatchQueue.main)
.subscribe(objectWillChange)
}
/////////////////////////////////////
static var shared = Settings()
#Setting(key: "isBla") var isBla = true
}
#propertyWrapper
public struct Setting<T> {
let key: String
let defaultValue: T
init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}
public var wrappedValue: T {
get {
let val = UserDefaults.standard.object(forKey: key) as? T
return val ?? defaultValue
}
set {
objectWillChange?.send()
publisher?.subject.value = newValue
UserDefaults.standard.set(newValue, forKey: key)
}
}
public struct Publisher: Combine.Publisher {
public typealias Output = T
public typealias Failure = Never
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Input == T, Downstream.Failure == Never {
subject.subscribe(subscriber)
}
fileprivate let subject: Combine.CurrentValueSubject<T, Never>
fileprivate init(_ output: Output) {
subject = .init(output)
}
}
private var publisher: Publisher?
internal var objectWillChange: ObservableObjectPublisher?
public var projectedValue: Publisher {
mutating get {
if let publisher = publisher {
return publisher
}
let publisher = Publisher(wrappedValue)
self.publisher = publisher
return publisher
}
}
}
I am trying to replicate the "Wizard School Signup"-example which was given in the WWDC 2019 session "Combine in Practice" https://developer.apple.com/videos/play/wwdc2019/721/ starting at 22:50 using SwiftUI (as opposed to UIKit, which was used during the session).
I have created all the publishers from the example: validatedEMail, validatedPassword and validatedCredentials. While validatedEMail and validatedPassword work just fine, validatedCredentials, which consumes both publishers using CombineLatest, never fires
//
// 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
I expected the form button to get enabled when a valid username (valid E-Mail-address) and two matching passwords with the right length are provided. The two Publishers responsible for those two tasks work, I can see the validatedEMail and the validatedPassword in the user interface in the two Texts which I added for debugging purposes.
Just the third Publisher (also compare to the code shown in the Video from above at 32:20) never fires. I did set breakpoints in those Publishers, in the validatedPassword Publisher at line:
guard password == passwordRepeat, password.count > 5 else { return nil }
which stopped there just fine but a similar breakpoint in the validatedCredentials Publisher at line:
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
was never reached.
What did I do wrong?
Edit:
In order to make the above code run under Xcode-beta 11.0 beta 4 didChange needs to be replaced with willChange
I've got this question answered here: https://forums.swift.org/t/crash-in-swiftui-app-using-combine-was-using-published-in-conjunction-with-state-in-swiftui/26628/9 by the very friendly and helpful Nanu Jogi, who is not on stackoverflow.
It is rather straight forward:
add this line:
.receive(on: RunLoop.main) // run on main thread
in validatedCredentials so that it looks like this:
var validatedCredentials: AnyPublisher<(String, String)?, Never> {
return Publishers.CombineLatest(validatedEMail, validatedPassword)
.receive(on: RunLoop.main) // <<—— run on main thread
.map { validatedEMail, validatedPassword in
print("validatedEMail: \(validatedEMail ?? "not set"), validatedPassword: \(validatedPassword ?? "not set")")
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
return (eMail, password)
}
.eraseToAnyPublisher()
This is all what is needed.
And here one more time the whole code for reference (updated for Xcode 11.0 beta 5 (11M382q)):
//
// RegistrationView.swift
// Combine-Beta-Feedback
//
// Created by Lars Sonchocky-Helldorf on 09.07.19.
// Copyright © 2019 Lars Sonchocky-Helldorf. All rights reserved.
//
import SwiftUI
import Combine
struct RegistrationView : View {
#ObservedObject var registrationModel = RegistrationModel()
#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.wrappedValue)
.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 match"
}
}
}
.navigationBarTitle(Text("Sign Up"))
}
func registrationButtonAction() {
}
}
class RegistrationModel : ObservableObject {
#Published var eMail: String = ""
#Published var password: String = ""
#Published var passwordRepeat: String = ""
var validatedEMail: AnyPublisher<String?, Never> {
return $eMail
.debounce(for: 0.5, scheduler: RunLoop.main)
.removeDuplicates()
.map { username in
return Future { promise in
print("username: \(username)")
self.usernameAvailable(username) { available in
promise(.success(available ? username : nil))
}
}
}
.switchToLatest()
.eraseToAnyPublisher()
}
var validatedPassword: AnyPublisher<String?, Never> {
return Publishers.CombineLatest($password, $passwordRepeat)
.debounce(for: 0.5, scheduler: RunLoop.main)
.map { password, passwordRepeat in
print("password: \(password), passwordRepeat: \(passwordRepeat)")
guard password == passwordRepeat, password.count > 5 else { return nil }
return password
}
.eraseToAnyPublisher()
}
var validatedCredentials: AnyPublisher<(String, String)?, Never> {
return Publishers.CombineLatest(validatedEMail, validatedPassword)
.receive(on: RunLoop.main)
.map { validatedEMail, validatedPassword in
print("validatedEMail: \(validatedEMail ?? "not set"), validatedPassword: \(validatedPassword ?? "not set")")
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
Just replace
.debounce(for: 0.5, scheduler: RunLoop.main)
with
.throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
Since there is no expensive code in the publishers subscription no deferred processing would be basically needed. Throttling the key events with latest: true will do the job almost in the same way.
I'm not such a Reactive programming expert that I can judge what is the reason behind, I assume a design choice.
You might need to group some of these publisher's validation into one consumer. There is a cool playground outlining the combine framework and this is how they do a similar use case. In the example they are validating the user name and password within the same subscriber. The subscriber does not execute until something has been published to the user name and password publishers.
If you wanted to keep them separate then you would need to add some more publishers that basically outline the state of whether the password is valid and the user name is valid. Then have subscribers listening to when both the user name and password publishers are valid.