How to Use Publishers.Combinelatest to Get 1 Publisher

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



Leave a reply



Submit