how to use Publishers.CombineLatest to get 1 publisher
A CurrentValueSubject
is:
A subject that wraps a single value and publishes a new element whenever the value changes.
Your canLogin
is certainly not a CurrentValueSubject
. It is the result of combining two other publishers with the CombineLatest
operator, and then mapping the combined publisher to yet another publisher.
In the language of the Swift type system, this kind of publisher is called:
Publishers.Map<Publishers.CombineLatest<Publishers.Map<CurrentValueSubject<String, Never>, Bool>, Publishers.Map<CurrentValueSubject<String, Never>, Bool>>, Bool>
Obviously, no one would declare a property with a type like that, so we use eraseToAnyPublisher
to get ourselves an AnyPublisher
, to say that we don't actually care what type of publisher it is.
let canLogin: AnyPublisher<Bool, Never>
...
canLogin = Publishers
.CombineLatest(isEmailValid, isPasswordCorrect)
.map { $0 && $1 }
.eraseToAnyPublisher()
Swift Combine using CombineLatest with optional publisher
CombineLatest
is designed to only emit once all of its upstream publishers have emitted their 1st value. If you want to emit as soon as any of the upstreams emitted and send nil
for all missing values, you can map all of your upstreams to emit Optional
values and force them to send nil
before any real values using prepend(nil)
.
This way, you'll receive a single [nil, nil, ...]
value before your real outputs (which you can ignore using dropFirst
) and then you'll start receiving your desired output.
let publisher: PassthroughSubject<Int, Never> = .init()
let publisher2: PassthroughSubject<Int, Never> = .init()
let optionalPublisher = publisher.map(Optional.init).prepend(nil)
let optionalPublisher2 = publisher2.map(Optional.init).prepend(nil)
let combined = Publishers.CombineLatest(optionalPublisher, optionalPublisher2)
combined.sink {
print("CombineLatest emitted \($0)")
}.store(in: &subscriptions)
publisher.send(1)
publisher2.send(1)
publisher.send(2)
Output:
CombineLatest emitted (nil, nil)
CombineLatest emitted (Optional(1), nil)
CombineLatest emitted (Optional(1), Optional(1))
CombineLatest emitted (Optional(2), Optional(1))
Swift Combine - combining publishers without waiting for all publishers to emit first element
You can use prepend(…)
to prepend values to the beginning of a publisher.
Here's a version of your code that will prepend nil
to both publishers.
let timer = Timer.publish(every: 10, on: .current, in: .common).autoconnect()
let anotherPub: AnyPublisher<Int, Never> = Just(10).delay(for: 5, scheduler: RunLoop.main).eraseToAnyPublisher()
Publishers.CombineLatest(
timer.map(Optional.init).prepend(nil),
anotherPub.map(Optional.init).prepend(nil)
)
.filter { $0 != nil && $1 != nil } // Filter the event when both are nil values
.sink(receiveValue: { (timer, val) in
print("Hello! \(timer) \(val)")
})
Combine Publishers.Merge but with Publishers.combineLatest behaviour
Assuming that your combine operation is just concat of the subarrays you can do:
let a = CurrentValueSubject<[String], Never>(["a", "b", "c"])
let b = CurrentValueSubject<[String], Never>(["d", "e", "f"])
let combined = Publishers.CombineLatest(a, b).map(+)
combined.sink {
print($0) //["a", "b", "c", "d", "e", "f"] and ["a", "b", "c", "g", "h", "i"]
}
b.send(["g", "h", "i"])
I am not completely sure what you mean with "already merged".
If you want to have the latest emitted array always at the end of the combined array then you might need a scan
operator before the map(+)
to be able to compare with previous emissions and swap them.
CombineLatest operator is not emitting when inners publishers use subscribe(on:)
Yes, it's a bug. We can simplify the test case to this:
import Combine
import Dispatch
let pub = Just("x")
.subscribe(on: DispatchQueue.main)
let ticket = pub.combineLatest(pub)
.sink(
receiveCompletion: { print($0) },
receiveValue: { print($0) })
This never prints anything. But if you comment out the subscribe(on:)
operator, it prints what's expected. If you leave subscribe(on:)
in, but insert some print()
operators, you'll see that the CombineLatest
operator never sends any demand upstream.
I suggest you copy the CombineX reimplementation of CombineLatest
and the utilities it needs to compile (the CombineX implementations of Lock
and LockedAtomic
, I think). I don't know that the CombineX version works either, but if it's buggy, at least you have the source and can try to fix it.
Swift Combine: subsequent Publisher that consumes other Publishers (using CombineLatest) doesn't fire
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
Related Topics
Reachability Change Notification Should Be Called Only Once
Ambiguous Reference to Member 'Buildblock()'
Module Compiled with Swift X.1 Cannot Be Imported in Swift X.0.2
Ckasset in Server Record Contains No Fileurl, Cannot Even Check for Nil
How to Use Crc32 from Zlib in Swift (Xcode 9)
Strange String.Unicodescalars and Characterset Behaviour
Bridging Header for Flurry.H Not Working with Pod
Detecting Swipes on All Four Directions on Watchkit Using The Storyboard
Cell Is Duplicated Multiple Times When Posting to Firebase
Access Id Does Not Work When Testing a Textfield with 'Issecuretextentry = True'
iOS 10 App Crashes When Accessing Camera
How to Get Textfields from Static Cells in UItableviewcontroller? Swift
Swift Codable: Decode Dictionary with Unknown Keys
Why Do I Have to Explicitly Unwrap My String in This Case