2

I want to create a Swift Combine publisher which achieves the following:

  • The publisher should be triggered by changes in either Defaults (a UserDefaults Swift package) or changes in GRDB sqlite database values (using GRDBCombine).
  • The updated UserDefaults received from the Defaults publisher should be used within the database query in the GRDBCombine publisher.

Here is a simplified version of what I have tried so far:

func tasksPublisher() -> AnyPublisher<[Task], Never> {
    Defaults.publisher(.myUserDefault)
        .flatMap { change in
            let myUserDefault = change.newValue

            return ValueObservation
                .tracking { db in
                    try Task.
                        .someFilter(myUserDefault)
                        .fetchAll(db)
                }
                .removeDuplicates()
                .publisher(in: database)
                .eraseToAnyPublisher()
        }
        .eraseToAnyPublisher()
}

However, this publisher produces the following error (edited according to the simplified version of my publisher above):

Cannot convert return expression of type 'AnyPublisher<Publishers.FlatMap<_, AnyPublisher<Defaults.KeyChange<Int>, Never>>.Output, Publishers.FlatMap<_, AnyPublisher<Defaults.KeyChange<Int>, Never>>.Failure>' (aka 'AnyPublisher<_.Output, Never>') to return type 'AnyPublisher<[Task], Never>'

My bet is that there is a problem with the two publishers having different values: [Task] and Defaults.KeyChange<Int>. However, I cannot find a way to work around this.

Simen
  • 417
  • 6
  • 13

1 Answers1

0

Assuming you want to start a new database publisher each time the Defaults publisher emits a change, you need the switchToLatest() operator.

This operator needs errors from both publishers to be harmonized. Here, since Defaults.publisher has the Never failure type, we can use the setFailureType(to:) operator in order to converge on the database publisher failure type: Error.

This gives:

func tasksPublisher() -> AnyPublisher<[Task], Error> {
    Defaults
        .publisher(.myUserDefault)
        .setFailureType(to: Error.self)
        .map({ change -> DatabasePublishers.Value<[Task]> in
            let myUserDefault = change.newValue
            return ValueObservation
                .tracking { db in
                    try Task
                        .someFilter(myUserDefault)
                        .fetchAll(db)
                }
                .removeDuplicates()
                .publisher(in: database)
        })
        .switchToLatest()
        .eraseToAnyPublisher()
}

Note that the returned publisher has the Error failure type, because the database is not 100% reliable, as all I/O externalities. It is difficult, in a Stack Overflow answer, to recommend hiding errors at this point (by turning them into an empty Task array, for example), because hiding errors prevents your app from knowing what's wrong and react accordingly.

Yet here is a version below that traps on database errors. This is the version I would use, assuming the app just can't run when SQLite does not work: it's sometimes useless to pretend such low-level errors can be caught and processed in a user-friendly way.

// Traps on database error
func tasksPublisher() -> AnyPublisher<[Task], Never> {
    Defaults
        .publisher(.myUserDefault)
        .map({ change -> AnyPublisher<[Task], Never> in
            let myUserDefault = change.newValue
            return ValueObservation
                .tracking { db in
                    try Task
                        .someFilter(myUserDefault)
                        .fetchAll(db)
                }
                .removeDuplicates()
                .publisher(in: database)
                .assertNoFailure("Unexpected database failure")
                .eraseToAnyPublisher()
        })
        .switchToLatest()
        .eraseToAnyPublisher()
}
Gwendal Roué
  • 3,949
  • 15
  • 34
  • Thanks for a great solution! However, `switchToLatest()` returns `Type of expression is ambiguous without more context`. But it worked perfectly without it. Regarding the error handeling you suggested: Is the purpose the same as [this line](https://github.com/groue/GRDBCombine/blob/1217e8735fbf352343ee8944dce693eabeb967d3/Documentation/Demo/GRDBCombineDemo/UI/HallOfFameViewModel.swift#L14) `.catch { _ in Empty() }` in GRDBCombine's demo app? – Simen Apr 23 '20 at 14:09
  • The sample codes I provide above do compile with Xcode 11.4, so I can't help regarding your `Type of expression is ambiguous without more context` error. This is a not a question about the Swift compiler, after all. Yes, I guess the `.catch { _ in Empty() }` plays the same role. Do not copy blindly: you have to figure out what your app needs first, evaluate your options, and write the code that expresses your intent. It takes time, but it's worth it. – Gwendal Roué Apr 23 '20 at 14:53
  • But shouldn't the absence of `switchToLatest()` result in some problems here, since we have two publishers in one? – Simen Apr 24 '20 at 19:41