1

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)

iOSGeek
  • 5,115
  • 9
  • 46
  • 74

1 Answers1

3

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()
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • You are right. Interesting that if I create a variable `let users = getUsers()` and use it in both branches of the if this does not work. I guess it's because when I create `let users = getUsers()` it will have a value of `[]` already – iOSGeek Jun 09 '20 at 19:40
  • 1
    Yes. I think your confusion with this and previous questions stems from the word `Future` - it doesn't actually happen in the "async" future - it could, but it fully depends on when the promise is set. Otherwise, it just copies the value when it runs. So your `getUsers` might as well have been `func getUsers() { Just(users) }` (plus the failure type setting) – New Dev Jun 09 '20 at 19:44