「Wizard School Signup」を複製しようとしています。WWDC2019セッション「Combine in Practice」で指定された例 https://developer.Apple.com/videos/play/wwdc2019/721/ = 22:50にSwiftUIを使用して開始(セッション中に使用された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
有効なユーザー名(有効なメールアドレス)と、適切な長さの2つの一致するパスワードが提供されたときに、フォームボタンが有効になることを期待していました。これらの2つのタスクを担当する2つのパブリッシャーが機能します。デバッグのために追加した2つのテキストのユーザーインターフェイスで、validatedEMailとvalidationdPasswordを確認できます。
3番目のパブリッシャーだけが(32:20の上のビデオに示されているコードと比較しても)起動しません。これらのパブリッシャー、次の行のvalidationdPasswordパブリッシャーにブレークポイントを設定しました。
guard password == passwordRepeat, password.count > 5 else { return nil }
これはそこで停止しましたが、次の行のvalidationdCredentialsパブリッシャーで同様のブレークポイントです。
guard let eMail = validatedEMail, let password = validatedPassword else { return nil }
到達することはなかった。
何を間違えたのですか?
編集:
上記のコードをXcode-beta 11.0ベータ4で実行するには、didChange
をwillChange
に置き換える必要があります。
交換するだけ
.debounce(for: 0.5, scheduler: RunLoop.main)
と
.throttle(for: 0.5, scheduler: RunLoop.main, latest: true)
パブリッシャーのサブスクリプションには高価なコードがないため、基本的に据え置き処理は必要ありません。 latest:trueを使用してキーイベントを調整すると、ほぼ同じように処理されます。
私は、理由が何であるかを判断できるほどのリアクティブプログラミングの専門家ではありません。設計上の選択を想定しています。
これらの発行者の検証の一部を1つのコンシューマーにグループ化する必要がある場合があります。結合フレームワークの概要を示すクールな遊び場があり、これが同様の ユースケース を行う方法です。この例では、同じサブスクライバー内のユーザー名とパスワードを検証しています。サブスクライバーは、ユーザー名とパスワードの発行者に何かが発行されるまで実行されません。
それらを別々に保持したい場合は、基本的にパスワードが有効でユーザー名が有効であるかどうかの状態を概説するいくつかの発行元を追加する必要があります。次に、サブスクライバーに、ユーザー名とパスワードの両方のパブリッシャーが有効になったときにリッスンしてもらいます。