I want to create a Swift Combine publisher which achieves the following:
UserDefaults
Swift package) or changes in GRDB sqlite database values (using GRDBCombine).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.
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()
}