Swift Combine:使用其他 Publisher 的后续 Publisher(使用 CombineLatest)不会 "fire"
Swift Combine: subsequent Publisher that consumes other Publishers (using CombineLatest) doesn't "fire"
我正在尝试使用 SwiftUI(从 22:50 开始复制 WWDC 2019 session "Combine in Practice" https://developer.apple.com/videos/play/wwdc2019/721/ 中给出的 "Wizard School Signup"-示例(与 session).
期间使用的 UIKit 相反
我已经根据示例创建了所有发布者:validatedEMail、validatedPassword 和 validatedCredentials。虽然 validatedEMail 和 validatedPassword 工作正常,但使用 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
我希望在提供有效用户名(有效 E-Mail-address)和两个长度正确的匹配密码时启用表单按钮。负责这两项任务的两个发布者工作,我可以在我为调试目的添加的两个文本中的用户界面中看到 validatedEMail 和 validatedPassword。
只有第三个 Publisher(也与上面 32:20 视频中显示的代码进行比较)永远不会触发。我确实在这些发布者中设置了断点,在行的 validatedPassword 发布者中:
guard password == passwordRepeat, password.count > 5 else { return nil }
在那里停得很好,但在 validatedCredentials Publisher 中有一个类似的断点:
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
从未达到。
我做错了什么?
编辑:
为了使运行在Xcode-beta11.0 beta 4 didChange
下的上述代码需要替换为willChange
您可能需要将其中一些发布者的验证分组到一个消费者中。有一个很酷的 playground 概述了 combine 框架,这就是他们如何做类似的 use case。在示例中,他们在同一订阅者中验证用户名和密码。在向用户名和密码发布者发布某些内容之前,订阅者不会执行。
如果您想将它们分开,那么您需要添加更多的发布者,这些发布者基本上概述了密码是否有效和用户名是否有效的状态。然后让订阅者在用户名和密码发布者都有效时监听。
只需更换
.debounce(for: 0.5, scheduler: RunLoop.main)
和
.throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
由于发布者订阅中没有昂贵的代码,因此基本上不需要延迟处理。使用 latest: true 节流关键事件将以几乎相同的方式完成工作。
我不是 React 编程专家,无法判断背后的原因,我假设是设计选择。
我在这里回答了这个问题:https://forums.swift.org/t/crash-in-swiftui-app-using-combine-was-using-published-in-conjunction-with-state-in-swiftui/26628/9 非常友好和乐于助人的 Nanu Jogi,他不在 Whosebug 上。
比较直接:
添加这一行:
.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
我正在尝试使用 SwiftUI(从 22:50 开始复制 WWDC 2019 session "Combine in Practice" https://developer.apple.com/videos/play/wwdc2019/721/ 中给出的 "Wizard School Signup"-示例(与 session).
期间使用的 UIKit 相反我已经根据示例创建了所有发布者:validatedEMail、validatedPassword 和 validatedCredentials。虽然 validatedEMail 和 validatedPassword 工作正常,但使用 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
我希望在提供有效用户名(有效 E-Mail-address)和两个长度正确的匹配密码时启用表单按钮。负责这两项任务的两个发布者工作,我可以在我为调试目的添加的两个文本中的用户界面中看到 validatedEMail 和 validatedPassword。
只有第三个 Publisher(也与上面 32:20 视频中显示的代码进行比较)永远不会触发。我确实在这些发布者中设置了断点,在行的 validatedPassword 发布者中:
guard password == passwordRepeat, password.count > 5 else { return nil }
在那里停得很好,但在 validatedCredentials Publisher 中有一个类似的断点:
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
从未达到。
我做错了什么?
编辑:
为了使运行在Xcode-beta11.0 beta 4 didChange
下的上述代码需要替换为willChange
您可能需要将其中一些发布者的验证分组到一个消费者中。有一个很酷的 playground 概述了 combine 框架,这就是他们如何做类似的 use case。在示例中,他们在同一订阅者中验证用户名和密码。在向用户名和密码发布者发布某些内容之前,订阅者不会执行。
如果您想将它们分开,那么您需要添加更多的发布者,这些发布者基本上概述了密码是否有效和用户名是否有效的状态。然后让订阅者在用户名和密码发布者都有效时监听。
只需更换
.debounce(for: 0.5, scheduler: RunLoop.main)
和
.throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
由于发布者订阅中没有昂贵的代码,因此基本上不需要延迟处理。使用 latest: true 节流关键事件将以几乎相同的方式完成工作。
我不是 React 编程专家,无法判断背后的原因,我假设是设计选择。
我在这里回答了这个问题:https://forums.swift.org/t/crash-in-swiftui-app-using-combine-was-using-published-in-conjunction-with-state-in-swiftui/26628/9 非常友好和乐于助人的 Nanu Jogi,他不在 Whosebug 上。
比较直接:
添加这一行:
.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