If you try this code in Playgroud:
import Combine
import Foundation
struct User {
let name: String
}
private var subscriptions = Set<AnyCancellable>()
var didAlreadyImportUsers = false
var users = [User]()
func importUsers() -> Future<Bool, Never> {
Future { promise in
DispatchQueue.global(qos: .userInitiated).async {
sleep(5)
users = [User(name: "John"), User(name: "Jack")]
promise(.success(true))
}
}
}
func getUsers(age: Int? = nil) ->Future<[User], Error> {
Future { promise in
promise(.success(users))
}
}
var usersPublisher: AnyPublisher<[User], Error> {
if didAlreadyImportUsers {
return getUsers().eraseToAnyPublisher()
} else {
return importUsers()
.setFailureType(to: Error.self)
.combineLatest(getUsers())
.map { $0.1 }
.eraseToAnyPublisher()
}
}
usersPublisher
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { value in
print(value)
}).store(in: &subscriptions)
It will print:
[]
finished
but I expect:
[User(name: "John"), User(name: "Jack")]
finished
If I remove the line with sleep(5)
then it prints the result correctly. It seems like an issue with asynchronicity. It seems like .combineLatest(getUsers())
is not waiting for importUsers()
I thought that combineLatest
is taking care of that ? what I'm missing here ?
(in my real code there is a long running Core Data operations instead of sleep
)
CombineLatest
would have waited, as you correctly expected, but in your case getUsers
already has a value ready, which was []
; i.e what users
was when getUsers
ran.
You don't actually need to use CombineLatest
to "wait" until some async action has happened. You can just chain publishers:
return importUsers()
.setFailureType(to: Error.self)
.flatMap { _ in
getUsers()
}
.eraseToAnyPublisher()
In fact, getUsers
isn't even needed, if you can assume that users
is populated after importUsers
:
return importUsers()
.map { _ in
self.users
}
.eraseToAnyPublisher()