【问题标题】:Swift Combine: subsequent Publisher that consumes other Publishers (using CombineLatest) doesn't "fire"Swift Combine:使用其他发布者(使用 CombineLatest)的后续发布者不会“触发”
【发布时间】:2019-11-16 12:16:11
【问题描述】:

我正在尝试复制 WWDC 2019 会议“Combine in Practice”https://developer.apple.com/videos/play/wwdc2019/721/ 中给出的“Wizard School Signup”示例,该示例从 22:50 开始使用 SwiftUI(而不是 UIKit,在会话)。

我已经创建了示例中的所有发布者:validatedEMail、validatedPassword 和 validCredentials。虽然validatedEMail 和validatePassword 工作得很好,但使用CombineLatest 消耗两个发布者的validatedCredentials 永远不会触发

//
//  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

我希望在提供有效的用户名(有效的电子邮件地址)和两个长度正确的匹配密码时启用表单按钮。负责这两个任务的两个 Publisher 工作,我可以在我添加用于调试目的的两个文本中的用户界面中看到验证电子邮件和验证密码。

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

guard password == passwordRepeat, password.count > 5 else { return nil }

它停在那里就好了,但在validatedCredentials Publisher中有一个类似的断点:

guard let eMail = validatedEMail, let password = validatedPassword else { return nil }

从未到达。

我做错了什么?

编辑:

为了让上面的代码在Xcode-beta 11.0 beta 4下运行,didChange需要替换成willChange

【问题讨论】:

    标签: swift swiftui ios13 combine


    【解决方案1】:

    我在这里得到了这个问题的回答:https://forums.swift.org/t/crash-in-swiftui-app-using-combine-was-using-published-in-conjunction-with-state-in-swiftui/26628/9 由非常友好和乐于助人的 Nanu Jogi 提供,他不在 stackoverflow 上。

    比较直接:

    添加这一行:

            .receive(on: RunLoop.main) // run on main thread 
    

    validatedCredentials 中,它看起来像这样:

    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()
    

    这就是我们所需要的。

    这里再来一次整个代码供参考(针对 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
    

    【讨论】:

      【解决方案2】:

      只需替换

      .debounce(for: 0.5, scheduler: RunLoop.main)
      

      .throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
      

      由于发布者订阅中没有昂贵的代码,因此基本上不需要延迟处理。使用 latest: true 限制关键事件几乎以相同的方式完成工作。

      我不是反应式编程专家,我可以判断背后的原因是什么,我假设一个设计选择。

      【讨论】:

        【解决方案3】:

        您可能需要将其中一些发布者的验证归为一个消费者。有一个很酷的游乐场概述了组合框架,这就是他们做类似use case 的方式。在示例中,他们正在验证同一订阅者中的用户名和密码。在向用户名和密码发布者发布某些内容之前,订阅者不会执行。

        如果您想将它们分开,那么您需要添加更多发布者,这些发布者基本上概述了密码是否有效以及用户名是否有效的状态。然后让订阅者在用户名和密码发布者都有效时进行监听。

        【讨论】:

        • 澄清一下:validatedEMailvalidatedPasswordpublishers 都工作并触发,正如我在正在收听它们的文本视图中看到的那样,当我运行调试会话时,我可以看到那里的价值观……
        • ... 这反过来又让我对此进行了更深入的思考:可能是一个发布者只能有一个订阅者。谷歌搜索带来了这个:developer.apple.com/documentation/combine/publisher/… 我的理论:如果有多播,那么可能有一些事实是发布者只允许有一个订阅者。我目前正在尝试解决这个问题。
        • 这可能是一个红鲱鱼,我试图在你指给我的操场上添加第二个订阅者,它成功了。
        • @SqAR.org 如果我错了,请纠正我,但在您的代码中 ``` Publishers.CombineLatest(validatedEMail,validatedPassword) ``` 是validatedPassword 和validateEmail 订阅者吗?在here 显示的操场上,他们似乎可以成为订阅者。在另一行你有 ``` Publishers.CombineLatest($password, $passwordRepeat) ``` 我肯定知道这两个是发布者。
        • 不,validatedPassword 和validatedEmail 是发布者。您示例中的订阅者是 .sink 。我通过注释掉 Text-View 订阅者,在发布者中设置一些断点并在模拟器中运行东西,在我的代码中进行了一些调试。所有的断点,也在validatedCredentials中到达,但只有一次:在validatedEMail中,每次我输入一封信时都会触发断点,而在其他两个发布者中,当我打开视图时,断点只触发一次......我完全迷失了。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-08-05
        • 1970-01-01
        • 2021-05-25
        • 1970-01-01
        • 2021-08-21
        • 2019-11-08
        相关资源
        最近更新 更多