Wrapping asynchronous code in Swift's Combine publisher - swift

I have a class called QueryObserver that can produce multiple results over time, given back as callbacks (closures). You use it like this:
let observer = QueryObserver<ModelType>(query: query) { result in
switch result {
case .success(let value):
print("result: \(value)")
case .failure(let error):
print("error: \(error)")
}
}
(QueryObserver is actually a wrapper around Firebase Firestore's unwieldy query.addSnapshotListener functionality, in case you were wondering. Using modern Result type instead of a callback with multiple optional parameters.)
In an older project I am using ReactiveKit and have an extension that turns all this into a Signal, like so:
extension QueryObserver {
public static func asSignal(query: Query) -> Signal<[T], Error> {
return Signal { observer in
let queryObserver = QueryObserver<T>(query: query) { result in
switch result {
case .success(let value):
observer.receive(value)
case .failure(let error):
if let firestoreError = error as? FirestoreError, case .noSnapshot = firestoreError {
observer.receive([])
} else {
observer.receive(completion: .failure(error))
}
}
}
return BlockDisposable {
queryObserver.stopListening()
}
}
}
}
In a brand new project though, I am using Combine and am trying to rewrite this. So far as I have managed to write this, but it doesn't work. Which makes sense: the observer is not retained by anything so it's immediately released, and nothing happens.
extension QueryObserver {
public static func asSignal(query: Query) -> AnyPublisher<[T], Error> {
let signal = PassthroughSubject<[T], Error>()
let observer = QueryObserver<T>(query: query) { result in
switch result {
case .success(let value):
print("SUCCESS!")
signal.send(value)
case .failure(let error):
if let firestoreError = error as? FirestoreError, case .noSnapshot = firestoreError {
signal.send([])
} else {
signal.send(completion: .failure(error))
}
}
}
return signal.eraseToAnyPublisher()
}
}
How do I make the Combine version work? How can I wrap existing async code? The only examples I found used Future for one-off callbacks, but I am dealing with multiple values over time.
Basically I am looking for the ReactiveKit-to-Combine version of this.

Check out https://github.com/DeclarativeHub/ReactiveKit/issues/251#issuecomment-575907641 for a handy Combine version of a Signal, used like this:
let signal = Signal<Int, TestError> { subscriber in
subscriber.receive(1)
subscriber.receive(2)
subscriber.receive(completion: .finished)
return Combine.AnyCancellable {
print("Cancelled")
}
}

Related

An Ordered chain of Publishers

I have two Publishers, I want to feed the second one with the result of first one, I could do what I wanted by calling the second one nested into the first one, it works but it does not feel good to look at, is there a better way to do it?
the first Publisher returns AnyPublisher<URL, Error> and the second one returns AnyPublisher<JsonModel, Error>
func upload(input: URL, output: URL) {
let converter = MP3Converter()
converter.convert(input: input, output: output)
.sink { [weak self] result in
if case .failure(let error) = result {
ConsoleLogger.log(error)
self?.uploadedRecordingURL = nil
}
} receiveValue: { url in
guard let data = try? Data(contentsOf: url) else {
return
}
self.repository.uploadCover(data: data)
.sink(receiveCompletion: { [weak self] result in
if case .failure(let error) = result {
ConsoleLogger.log(error)
self?.uploadedRecordingURL = nil
}
}, receiveValue: { [weak self] response in
self?.uploadedRecordingURL = response.fileURL
}).store(in: &self.disposables)
}.store(in: &disposables)
}
I think you've already deduced this, but just do be clear: you are using sink and its receiveValue completely wrong. Don't start a new chain or do any significant work here at all! There should be a simple sink at the end, followed by store to anchor the chain, and that's the end.
You are looking for flatMap. That is how you chain publishers. (See my https://www.apeth.com/UnderstandingCombine/operators/operatorsTransformersBlockers/operatorsflatmap.html.) You may have to give some thought to exactly what needs to pass from the first publisher and its chain down into the flatMap closure and what needs to pass on down the chain from there.
You could use flatmap to chain your publishers together.
// the code would roughly look like this
converter.convert(input: input, output: output).flatMap { data in
return self.repository.uploadCover(data: data)
}.sink(receiveCompletion: { [weak self] result in
if case .failure(let error) = result {
ConsoleLogger.log(error)
self?.uploadedRecordingURL = nil
}
}, receiveValue: { [weak self] response in
self?.uploadedRecordingURL = response.fileURL
}).store(in: &self.disposables)
Here's an example in playgrounds that compiles
import Combine
import Foundation
enum SomeError: Error {
}
let subject0 = CurrentValueSubject<Data, SomeError>(Data())
let pub0 = subject0.eraseToAnyPublisher()
func repoUpload(data: Data) -> AnyPublisher<URL, SomeError> {
// do the real work, this is just to get it to compile
let subject1 = CurrentValueSubject<URL, SomeError>(URL(fileURLWithPath: "Somepath"))
return subject1.eraseToAnyPublisher()
}
var disposables = Set<AnyCancellable>()
pub0.flatMap { data in
return repoUpload(data: data)
}.sink(receiveCompletion: { result in
print(result)
}, receiveValue: { response in
print(response)
}).store(in: &disposables)

Polymorphism with a final class that implements an associatedtype protocol in swift

I'm using Apollo v0.49.0. It's a library for calling graphQL endpoints, and the way it does this is by generating code before you compile your code.
Before I talk about the generated code, I'd like to talk about what the generated code implements. For this question, it's the GraphQLMutation that's relevant. Here's what it looks like:
public enum GraphQLOperationType {
case query
case mutation
case subscription
}
public protocol GraphQLOperation: AnyObject {
var operationType: GraphQLOperationType { get }
var operationDefinition: String { get }
var operationIdentifier: String? { get }
var operationName: String { get }
var queryDocument: String { get }
var variables: GraphQLMap? { get }
associatedtype Data: GraphQLSelectionSet
}
public extension GraphQLOperation {
var queryDocument: String {
return operationDefinition
}
var operationIdentifier: String? {
return nil
}
var variables: GraphQLMap? {
return nil
}
}
public protocol GraphQLQuery: GraphQLOperation {}
public extension GraphQLQuery {
var operationType: GraphQLOperationType { return .query }
}
public protocol GraphQLMutation: GraphQLOperation {}
public extension GraphQLMutation {
var operationType: GraphQLOperationType { return .mutation }
}
This is 80% of the file; the last 20% is irrelevant IMHO. Note how GraphQLMutation implements GraphQLOperation and the latter has an associatedtype.
The library generates classes based on your graphql server endpoints. Here's what they look like:
public final class ConcreteMutation: GraphQLMutation {
...
public struct Data: GraphQLSelectionSet {
...
}
...
}
As far as I know (I'm new to Swift), I have no control over any of the code I've mentioned so far (other than forking the repo and modifying it). I could change them locally, but they would just be overridden every time they were regenerated.
To use any of these generated classes, I have to pass them into this ApolloClient function (also a library class):
#discardableResult
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
publishResultToStore: Bool = true,
queue: DispatchQueue = .main,
resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
return self.networkTransport.send(
operation: mutation,
cachePolicy: publishResultToStore ? .default : .fetchIgnoringCacheCompletely,
contextIdentifier: nil,
callbackQueue: queue,
completionHandler: { result in
resultHandler?(result)
}
)
}
I can't figure out how to deal with ConcreteMutation in a generic way. I want to be able to write a factory function like so:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
The fact that this func is in an enum is irrelevant to me: that same code could be in a struct/class/whatever. What matters is the function signature. I want a factory method that returns a GraphQLMutation that can be passed into ApolloClient.perform()
Because I can't figure out a way to do either of those things, I end up writing a bunch of functions like this instead:
func useConcreteMutation1(value) -> Void {
let mutation = ConcreteMutation1(first_name: value)
apolloClient.perform(mutation: mutation)
}
func useConcreteMutation2(value) -> Void {
let mutation = ConcreteMutation2(last_name: value)
apolloClient.perform(mutation: mutation)
}
...
That's a lot of duplicated code.
Depending on how I fiddle with my getMutation signature -- e.g., <T: GraphQLMutation>() -> T? etc. -- I can get the func to compile, but I get a different compile error when I try to pass it into ApolloClient.perform(). Something saying "protocol can only be used as a generic constraint because it has Self or associated type requirements."
I've researched this a lot, and my research found this article, but I don't think it's an option if the concrete classes implementing the associated type are final?
It's really difficult to figure out if it's possible to use polymorphism in this situation. I can find plenty of articles of what you can do, but no articles on what you can't do. My question is:
How do I write getMutation so it returns a value that can be passed into ApolloClient.perform()?
The fundamental problem you are running into is that this function signature:
func getMutation<T: GraphQLMutation>() -> T
is ambiguous. The reason it's ambiguous is because GraphQLMutation has an associated type (Data) and that information doesn't appear anywhere in your function declaration.
When you do this:
extension SomeEnum {
func getMutation<T: GraphQLMutation>() -> T {
switch self {
case .a:
return ConcreteMutation1(first_name: value) as T
case .b:
return ConcreteMutation2(last_name: value) as T
case .c:
return ConcreteMutation3(bio: value) as T
...
}
}
}
Each of those branches could have a different type. ConcreteMutation1 could have a Data that is Dormouse while ConcreteMutation3 might have a data value that's an IceCreamTruck. You may be able to tell the compiler to ignore that but then you run into problems later because Dormouse and IceCreamTruck are two structs with VERY different sizes and the compiler might need to use different strategies to pass them as parameters.
Apollo.perform is also a template. The compiler is going to write a different function based on that template for each type of mutation you call it with. In order to do that must know the full type signature of the mutation including what its Data associated type is. Should the responseHandler callback be able to handle something the size of a Dormouse, or does it need to be able to handle something the size of an IceCreamTruck?
If the compiler doesn't know, it can't set up the proper calling sequence for the responseHandler. Bad things would happen if you tried to squeeze something the size of an IceCreamTruck through a callback calling sequence that was designed for a parameter the size of a Dormouse!
If the compiler doesn't know what type of Data the mutation has to offer, it can't write a correct version of perform from the template.
If you've only handed it the result of func getMutation<T: GraphQLMutation>() -> T where you've eliminated evidence of what the Data type is, it doesn't know what version of perform it should write.
You are trying to hide the type of Data, but you also want the compiler to create a perform function where the type of Data is known. You can't do both.
Maybe you need to implement AnyGraphQLMutation type erased over the associatedtype.
There are a bunch of resources online for that matter (type erasure), I've found this one pretty exhaustive.
I hope this helps in someway:
class GraphQLQueryHelper
{
static let shared = GraphQLQueryHelper()
class func performGraphQLQuery<T:GraphQLQuery>(query: T, completion:#escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().fetch(query: query, cachePolicy: .default) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle Network error*/
break
}
}
}
class func peroformGraphQLMutation<T:GraphQLMutation>(mutation: T, completion:#escaping(GraphQLSelectionSet) -> ())
{
Network.shared.apollo().perform(mutation: mutation) { (result) in
switch result
{
case .success(let res):
if let data = res.data
{
completion(data)
}
else if let error = res.errors?.first
{
if let dict = error["extensions"] as? NSDictionary
{
switch dict.value(forKey: "code") as? String ?? "" {
case "invalid-jwt": /*Handle Refresh Token Expired*/
default: /*Handle error*/
break
}
}
else
{
/*Handle error*/
}
}
else
{
/*Handle Network error*/
}
break
case .failure(let error):
/*Handle error*/
break
}
}
}
}

Replace nil with Empty or Error in Combine

I have a Combine publisher like this:
enum RemoteError: Error {
case networkError(Error)
case parseError(Error)
case emptyResponse
}
func getPublisher(url: URL) -> AnyPublisher<Entiy, RemoteError> {
return URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: RemoteResponse.self, decoder: decoder)
.mapError { error -> RemoteError in
switch error {
case is URLError:
return .networkError(error)
default:
return .parseError(error)
}
}
.map { response -> Entiy in
response.enitities.last
}
.eraseToAnyPublisher()
}
struct RemoteResponse: Codable {
let enitities: [Entity]
let numberOfEntries: Int
}
struct Entity {
}
By the above setting, the compiler complains because response.enitities.last can be nil. The question is can I replace nil with Empty publisher and if not can I replace it with error emptyResponse in Combine chain? The first option is preferable.
You have a couple of options here.
If you don't want the publisher to publish anything in case entities is empty, you can use coampactMap instead of map:
.compactMap { response in
response.entities.last
}
If you would rather publish an error in such a case you can use tryMap which allows you to throw an Error. You would need mapError to come after it:
.tryMap { response in
guard let entity = response.entities.last else {
throw RemoteError.emptyResponse
}
return entity
}
.mapError { error -> RemoteError in
switch error {
case is URLError:
return .networkError(error)
case is DecodingError:
return .parseError(error)
default:
return .emptyResponse
}
}
You need a flat map in order to map to another publisher:
.flatMap {
$0.enitities.last.publisher
}
Optional has a convenient publisher property that gives you a publisher that publishes only that value if the value is not nil, and an empty publisher if it is nil. This is only available in iOS 14+. If you are targeting a lower version, you need to do something like:
.flatMap { (response) -> AnyPublisher<Entity, Never> in
if let last = response.entities.last {
return Just(last).eraseToAnyPublisher()
} else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
}
Following the answer from #Sweeper, Here edit with below iOS 14.0
.setFailureType(to: NSError.self)
.flatMap { (response) -> AnyPublisher<Entity, Never> in
if let last = response.entities.last {
return Just(last).eraseToAnyPublisher()
} else {
return Empty(completeImmediately: true).eraseToAnyPublisher()
}
NSError -> will be error type which you are returning
Happy coding 🚀

Nested Cloudkit queries not printing in the right order

My code is not printing in the right order. I think its a problem with the way I'm using dispatch to main. I want to pass in a boss ID. Find the User with that ID then get their SubscribedBosses Array. For every ID in that array query that user and return that users screenName. Append those screenName to the userArray. After that Complete the GetBossSubs function and return userArray (array of screenNames).
Right now result of the .onAppear is running before the function in .onAppear is actually completed. User Appended prints before Found TestUserName
static func getBossSubs(bossID: String, completion: #escaping (Result<UserNames, Error>) ->
()) {
let pred = NSPredicate(format: "uniqueID = %#", bossID)
let sort = NSSortDescriptor(key: "creationDate", ascending: false)
let query = CKQuery(recordType: RecordType.Users, predicate: pred)
query.sortDescriptors = [sort]
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["subscribedBosses"]
operation.resultsLimit = 50
operation.recordFetchedBlock = { record in
DispatchQueue.main.async {
guard let subs = record["subscribedBosses"] as? [String] else {
print("Error at screenName")
completion(.failure(CloudKitHelperError.castFailure))
return
}
let userArray = UserNames() //Error that it should be a LET is here.
for boss in subs{
CloudKitHelper.getBossScreenName(bossID: boss) { (result) in
switch result{
case .success(let name):
userArray.names.append(name) //works fine
print("Found \(userArray.names)") //Prints a name
case .failure(let er):
completion(.failure(er))
}
}
}
print("does this run?") // Only runs if No Name
completion(.success(userArray)) // contains no name or doesn't run?
}
}
operation.queryCompletionBlock = { (_, err) in
DispatchQueue.main.async {
if let err = err {
completion(.failure(err))
return
}
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
I call the code like this:
.onAppear {
// MARK: - fetch from CloudKit
self.userList.names = []
let myUserID = UserDefaults.standard.string(forKey: self.signInWithAppleManager.userIdentifierKey)!
// get my subs projects
CloudKitHelper.getBossSubs(bossID: myUserID) { (results) in
switch results{
case .success(let user):
print("Users appended")
self.userList.names = user.names
case .failure(let er):
print(er.localizedDescription)
}
}
}
This is because you run an asynchronous function inside another asynchronous function.
// this just *starts* a series of asynchronous functions, `result` is not yet available
for boss in subs {
CloudKitHelper.getBossScreenName(bossID: boss) { result in
switch result {
case .success(let name):
userArray.names.append(name) // works fine
print("Found \(userArray.names)") // Prints a name
case .failure(let er):
completion(.failure(er))
}
}
}
// this will run before any `CloudKitHelper.getBossScreenName` finishes
completion(.success(userArray))
A possible solution may be to check if all CloudKitHelper.getBossScreenName functions finish (eg. by checking the size of the userArray) and only then return the completion:
for boss in subs {
CloudKitHelper.getBossScreenName(bossID: boss) { result in
switch result {
case .success(let name):
userArray.names.append(name)
if userArray.names.count == subs.count {
completion(.success(userArray)) // complete only when all functions finish
}
case .failure(let er):
completion(.failure(er))
}
}
}
// do not call completion here, wait for all functions to finish
// completion(.success(userArray))

Prevent Observabe.error complete and dispose swift

I have an observable (request from network) and dont want it to be disposed when I got an error
My custom error
enum MyError: Error {
case notFound
case unknown
}
My network request using Moya
let registerRequest = didTapJoinButton.withLatestFrom(text.asObservable())
.flatMapLatest { text in
provider.rx.request(API.register(text: text))
}
.flatMapLatest({ (response) -> Observable<Response> in
let statusCode = response.statusCode
if statusCode.isSuccessStatus() {
return Observable.just(response)
} else if statusCode.isNotFoundStatus() {
return Observable.error(MyError.notFound)
} else {
return Observable.error(MyError.unknown)
}
})
.materialize()
.share(replay: 1)
Looks great. I use materialize() to prevent observable being disposed on error
Subscribe: (If status code 200)
Everything works just fine, I got response and the stream is not disposed
registerEventRequest.subscribe(onNext: { (next) in
print("NEXT: \(next)")
}, onError: { (error) in
print("ERRRRROR ME: \(error)")
}, onCompleted: {
print("Completed")
}) {
print("Disposed")
}
BUT if status code is something like 404. I got the error as I expected. However, hey look at the console log
NEXT: error(notFound)
Completed
Disposed
It jumps to NEXT which I expected. But why it throw complete and dispose my sequence.
My question is Why did it dispose my sequence and how I can prevent this?
.materialize() does not prevent an observable from being disposed on error. When an Observable emits an error, it is finished and materialize merely converts that error into a next event.
You need to put the materialize inside the first flatMapLatest to prevent the error from escaping the flatMap closure.
This video might help (note selectMany is the same as flatMap) https://channel9.msdn.com/Blogs/J.Van.Gogh/Reactive-Extensions-API-in-depth-SelectMany?term=select%20many&lang-en=true
Here's another way to compose the Observable:
let registerRequest = didTapJoinButton.withLatestFrom(text.asObservable())
.flatMapLatest { text in
provider.rx.request(API.register(text: text))
.materialize()
}
.map { (event) -> Event<Response> in
switch event {
case .next(let response) where response.statusCode.isNotFoundStatus():
return Event.error(MyError.notFound)
case .next(let response) where response.statusCode.isSuccessStatus() == false:
return Event.error(MyError.unknown)
default:
return event
}
}
.share(replay: 1)
I moved materialize() where it belongs so errors won't break the chain. I also swapped out the second flatMapLatest for a simple map since the extra work wasn't necessary.
The switch statement could have also been written like this:
switch event {
case .next(let response):
let statusCode = response.statusCode
if statusCode.isNotFoundStatus() {
return Event.error(MyError.notFound)
}
else if statusCode.isSuccessStatus() {
return event
}
else {
return Event.error(MyError.unknown)
}
default:
return event
}
But I think the way I did it is cleaner because it reduces the cyclomatic complexity of the closure.
Here is code to deal with the concern brought up in the comments:
extension ObservableType {
func flatMapLatestT<T, U>(_ selector: #escaping (T) -> Observable<U>) -> Observable<Event<U>>
where Self.E == Event<T>
{
return self.flatMapLatest { (event) -> Observable<Event<U>> in
switch event {
case .next(let element):
return selector(element).materialize()
case .completed:
return .just(Event<U>.completed)
case .error(let error):
return .just(Event<U>.error(error))
}
}
}
}
This gist contains a whole suite of operators for dealing with Events. https://gist.github.com/dtartaglia/d7b8d5c63cf0c91b85b629a419b98d7e