快速合并:使用其他合并(使用CombineLatest)的其他发布者的后续发布者不会“解雇" [英] Swift Combine: subsequent Publisher that consumes other Publishers (using CombineLatest) doesn't "fire"

查看:54
本文介绍了快速合并:使用其他合并(使用CombineLatest)的其他发布者的后续发布者不会“解雇"的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试复制WWDC 2019会议实践结合"中给出的向导学校注册"示例 ////RegistrationView.swift////由Lars Sonchocky-Helldorf在19年4月7日创建.//版权所有©2019 Lars Sonchocky-Helldorf.版权所有.//导入SwiftUI进口联合收割机struct RegistrationView:查看{@ObjectBinding var registrationModel = RegistrationModel()@State私人var showAlert = false@State私人var alertTitle:字符串="@State私人var alertMessage:字符串="@State私有var registrationButtonDisabled = true@State私人var validatedEMail:字符串="@State私人var validatedPassword:字符串="var body:some View {形式 {部分 {TextField(输入您的电子邮件",文本:$ registrationModel.eMail)SecureField(输入密码",文本:$ registrationModel.password)SecureField(再次输入密码",文本:$ registrationModel.passwordRepeat)Button(操作:registrationButtonAction){文字(创建帐户")}.disabled($ registrationButtonDisabled.value).presentation($ showAlert){Alert(标题:Text("\(alertTitle)"),消息:Text("\(alertMessage)"))}.onReceive(self.registrationModel.validatedCredentials){newValidatedCredentials inself.registrationButtonDisabled =(newValidatedCredentials ==零)}}部分 {文本(验证的电子邮件:\(validatedEMail)").onReceive(self.registrationModel.validatedEMail){self.validatedEMail = newValidatedEMail!=无吗?newValidatedEMail!:发送电子邮件无效"}文本(验证的密码:\(validatedPassword)").onReceive(self.registrationModel.validatedPassword){self.validatedPassword = newValidatedPassword!=无?newValidatedPassword!:密码短或不匹配"}}}.navigationBarTitle(Text("Sign Up"))}func registrationButtonAction(){let trimmedEMail:字符串= self.registrationModel.eMail.trimmingCharacters(在:.whitespaces中)if(trimmedEMail!="&& self.registrationModel.password!="){NetworkManager.sharedInstance.registerUser(NetworkManager.RegisterRequest(uid:trimmedEMail,password:self.registrationModel.password)){(状态)在如果状态== 200 {self.showAlert = trueself.alertTitle = NSLocalizedString(注册成功",注释:")self.alertMessage = NSLocalizedString(请验证您的电子邮件和登录名,注释:")}否则,如果status == 400 {self.showAlert = trueself.alertTitle = NSLocalizedString("Registration Error",注释:")self.alertMessage = NSLocalizedString(已经注册",注释:")} 别的 {self.showAlert = trueself.alertTitle = NSLocalizedString("Registration Error",注释:")self.alertMessage = NSLocalizedString(网络或应用程序错误",注释:")}}} 别的 {self.showAlert = trueself.alertTitle = NSLocalizedString("Registration Error",注释:")self.alertMessage = NSLocalizedString(用户名/密码为空",注释:")}}}类RegistrationModel:BindableObject {@Published var eMail:字符串="@已发布的var密码:字符串="@Published var passwordRepeat:字符串="公共var didChange = PassthroughSubject< Void,Never>()var validatedEMail:AnyPublisher< String ?,从不>{返回$ eMail.debounce(for:0.5,scheduler:RunLoop.main).removeDuplicates().flatMap {返回Future {承诺于self.usernameAvailable(username){在以下版本中可用promise(.success(可用?用户名:nil))}}}.eraseToAnyPublisher()}var validatedPassword:AnyPublisher< String ?,从不>{返回Publishers.CombineLatest($ password,$ passwordRepeat).debounce(for:0.5,scheduler:RunLoop.main).map {密码,密码重复输入保护密码== passwordRepeat,password.count>5 else {return nil}返回密码}.eraseToAnyPublisher()}var validatedCredentials:AnyPublisher<(String,String)?, Never>{返回Publishers.CombineLatest(validatedEMail,validatedPassword).map {守卫let eMail = validatedEMail,let password = validatedPassword else {return nil}返回(电子邮件,密码)}.eraseToAnyPublisher()}func usernameAvailable(_ username:String,complete:(Bool)-> Void){let isValidEMailAddress:Bool = NSPredicate(格式:"SELF MATCHES%@","[A-Z0-9a-z ._%+-] + @ [A-Za-z0-9 .-] + \\.[A-Za-z] {2,64}).evaluate(使用:用户名)完成(isValidEMailAddress)}}#if调试struct RegistrationView_Previews:PreviewProvider {静态var预览:某些视图{RegistrationView()}}#万一

我希望提供有效的用户名(有效的电子邮件地址)和两个正确长度的匹配密码时,启用表单按钮.负责这两个任务的两个发布者可以正常工作,我可以在用户界面的两个文本中看到validatedEMail和validatedPassword,我添加了这两个文本是为了调试.

第三个发布者(也与上面32:20的视频中显示的代码进行比较)永远不会触发.我确实在这些发布者的validatedPassword Publisher中设置了断点:

 保护密码== passwordRepeat,password.count>5 else {return nil} 

在此停下来就好了,但是在validatedCredentials Publisher的以下行有一个类似的断点:

  guard设置让电子邮件= validatedEMail,让密码= validatedPassword否则{return nil} 

从未到达.

我做错了什么?

为了使上述代码在Xcode-beta 11.0 beta 4下运行,需要将 didChange 替换为 willChange

我在这里回答了这个问题:

这就是所需要的.

这里又是整个代码的参考时间(已针对Xcode 11.0 beta 5(11M382q)更新):

 ////RegistrationView.swift//Combine-Beta-Feedback////由Lars Sonchocky-Helldorf在09.07.19创建.//版权所有©2019 Lars Sonchocky-Helldorf.版权所有.//导入SwiftUI进口联合收割机struct RegistrationView:查看{@ObservedObject var registrationModel = RegistrationModel()@State私有var registrationButtonDisabled = true@State私人var validatedEMail:字符串="@State私人var validatedPassword:字符串="var body:some View {形式 {部分 {TextField(输入您的电子邮件",文本:$ registrationModel.eMail)SecureField(输入密码",文本:$ registrationModel.password)SecureField(再次输入密码",文本:$ registrationModel.passwordRepeat)Button(操作:registrationButtonAction){文字(创建帐户")}.disabled($ registrationButtonDisabled.wrappedValue).onReceive(self.registrationModel.validatedCredentials){newValidatedCredentials inself.registrationButtonDisabled =(newValidatedCredentials ==零)}}部分 {文本(验证的电子邮件:\(validatedEMail)").onReceive(self.registrationModel.validatedEMail){self.validatedEMail = newValidatedEMail!=无吗?newValidatedEMail!:发送电子邮件无效"}文本(验证的密码:\(validatedPassword)").onReceive(self.registrationModel.validatedPassword){self.validatedPassword = newValidatedPassword!=无?newValidatedPassword!:密码不足或不匹配的密码"}}}.navigationBarTitle(Text("Sign Up"))}func registrationButtonAction(){}}类RegistrationModel:ObservableObject {@Published var eMail:字符串="@已发布的var密码:字符串="@Published var passwordRepeat:字符串="var validatedEMail:AnyPublisher< String ?,从不>{返回$ eMail.debounce(for:0.5,scheduler:RunLoop.main).removeDuplicates().map {返回Future {承诺于打印(用户名:\(用户名)")self.usernameAvailable(username){在以下版本中可用promise(.success(可用?用户名:nil))}}}.switchToLatest().eraseToAnyPublisher()}var validatedPassword:AnyPublisher< String ?,从不>{返回Publishers.CombineLatest($ password,$ passwordRepeat).debounce(for:0.5,scheduler:RunLoop.main).map {密码,密码重复输入打印(密码:\(密码),密码重复:\(密码重复)")保护密码== passwordRepeat,password.count>5 else {return nil}返回密码}.eraseToAnyPublisher()}var validatedCredentials:AnyPublisher<(String,String)?, Never>{返回Publishers.CombineLatest(validatedEMail,validatedPassword).receive(在:RunLoop.main上).map {print("validatedEMail:\(validatedEMail ??"未设置),validatedPassword:\(validatedPassword ??"未设置)")守卫let eMail = validatedEMail,let password = validatedPassword else {return nil}返回(电子邮件,密码)}.eraseToAnyPublisher()}func usernameAvailable(_ username:String,complete:(Bool)-> Void){let isValidEMailAddress:Bool = NSPredicate(格式:"SELF MATCHES%@","[A-Z0-9a-z ._%+-] + @ [A-Za-z0-9 .-] + \\.[A-Za-z] {2,64}).evaluate(使用:用户名)完成(isValidEMailAddress)}}#if调试struct RegistrationView_Previews:PreviewProvider {静态var预览:某些视图{RegistrationView()}}#万一 

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

这篇关于快速合并:使用其他合并(使用CombineLatest)的其他发布者的后续发布者不会“解雇"的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆