Swift: execute an operation conditionally on Publisher - swift

I'm using Combine for networking and I want to have some of the combine operations executed only if a certain condition is present, for instance if a local boolean variable needsDecoding is true I want to add .decode(type:,decoder:):
if (needsDecoding) {
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.decode(type: T.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
} else {
return URLSession.shared.dataTaskPublisher(for: request)
.map(\.data)
.eraseToAnyPublisher()
}
That code above works but is it possible to conditionally add .decode() to the chain?

Assume for the moment that we try to write a generic function that does the same thing, accepts some data and returns either a generic parsed type or the original data:
func conditionallyDecodeSomeData<T>(data:Data) -> What_Goes_Here? {
...
}
Your question is pretty similar to "What type should this function return". The answer is the type that is the most general superset of all possible results. That's probably Any. (though you want it in a Result monad so it's probably Result<Any, Error>). Using Any would work, but its a pretty bad 'code smell'
If you have a limited number of return types then you could create an enumerated type:
enum OperationResults {
case justData(Data)
case oneParsedResult(Result1)
case anotherParsedResult(AnotherResult)
}
Then your Combine pipeline could produce AnyPublisher<OperationResults, Error>. But if you want something truly generic then you would probably have to use AnyPublisher<Any, Error> which is not satisfying.

Related

Combine sink: ignore receiveValue, only completion is needed

Consider the following code:
CurrentValueSubject<Void, Error>(())
.eraseToAnyPublisher()
.sink { completion in
switch completion {
case .failure(let error):
print(error)
print("FAILURE")
case .finished:
print("SUCCESS")
}
} receiveValue: { value in
// this should be ignored
}
Just by looking at the CurrentValueSubject initializer, it's clear that the value is not needed / doesn't matter.
I'm using this particular publisher to make an asynchronous network request which can either pass or fail.
Since I'm not interested in the value returned from this publisher (there are none), how can I get rid of the receiveValue closure?
Ideally, the call site code should look like this:
CurrentValueSubject<Void, Error>(())
.eraseToAnyPublisher()
.sink { completion in
switch completion {
case .failure(let error):
print(error)
print("FAILURE")
case .finished:
print("SUCCESS ")
}
}
It also might be the case that I should use something different other than AnyPublisher, so feel free to propose / rewrite the API if it fits the purpose better.
The closest solution I was able to find is ignoreOutput, but it still returns a value.
You could declare another sink with just completion:
extension CurrentValueSubject where Output == Void {
func sink(receiveCompletion: #escaping ((Subscribers.Completion<Failure>) -> Void)) -> AnyCancellable {
sink(receiveCompletion: receiveCompletion, receiveValue: {})
}
}
CurrentValueSubject seems a confusing choice, because that will send an initial value (of Void) when you first subscribe to it.
You could make things less ambiguous by using Future, which will send one-and-only-one value, when it's done.
To get around having to receive values you don't care about, you can flip the situation round and use an output type of Result<Void, Error> and a failure type of Never. When processing your network request, you can then fulfil the promise with .failure(error) or .success(()), and deal with it in sink:
let pub = Future<Result<Void, Error>, Never> {
promise in
// Do something asynchronous
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
promise(.success(.success(())))
//or
//promise(.success(.failure(error)))
}
}.eraseToAnyPublisher()
// somewhere else...
pub.sink {
switch $0 {
case .failure(let error):
print("Whoops \(error)")
case .success:
print("Yay")
}
}
You're swapping ugly code at one end of the chain for ugly code at the other, but if that's hidden away behind AnyPublisher and you're concerned with correct usage, that seems the way to go. Consumers can see exactly what to expect from looking at the output and error types, and don't have to deal with them in separate closures.

What is the right approach for resolving save operations with Swift Combine

I have a classic situation where I want to commit some action only once. For example update some entity in database using Swift Combine. My problem is that I don't really know what is the best approach for doing something only once. How do I unsubscribe when the update is finished?
This is code snippet through the layers that I am currently using:
ViewModel:
let settingsModel: LocalSettingsModel
func saveLocalSettings(){
let cancelable = settingsUseCase
.saveLocalSettings(localSettingsModel: settingsModel)
.sink(receiveCompletion: {_ in
print("Completed!!!")
}) { _ in
print("Result of Save operation!!!")
}
}
UseCase:
func saveLocalSettings(settings: LocalSettingsModel) -> AnyPublisher<LocalSettingsModel, Error> {
return repository.saveLocalSettings(settings: settings)
}
Repository:
guard let realmSettings = LocalSettingsRealmModel(fromModel: settings) else {
return Fail<LocalSettingsModel, Error>(error: .postconditionError(errorMessage: ""))
.eraseToAnyPublisher()
}
return self.localDataSource
.saveLocalSettings(localSettings: realmSettings)
.receive(on: DispatchQueue.main)
.subscribe(on: DispatchQueue.global())
.mapError { (error) -> Error in
// do error mapping
}
.compactMap { settings in
return (LocalSettingsModel(fromModel: settings))
}
.eraseToAnyPublisher()
Data Source:
func saveLocalSettings(localSettings: LocalSettingsRealmModel) -> AnyPublisher<LocalSettingsRealmModel, LocalDataSourceError> {
do {
return Just(try saveSettings(localSettings: localSettings))
.mapError({ (Never) -> LocalDataSourceError in})
.eraseToAnyPublisher()
} catch let error as NSError {
// return some error
}
}
func saveSettings(localSettings: LocalSettingsRealmModel) throws -> LocalSettingsRealmModel
{
let realm = try Realm()
try realm.write {
realm.add(localSettings, update: .modified)
}
return localSettings
}
I would really appreciate some pointers in the direction of what is a good practice when we are not expecting continuous stream of information in reactive world like in case of functions whose purpose is to execute single action. Do I really need to use Just() like in this example or is there a way do deal with this kind of situations from subscriber side.
You want something that will convert a Publisher into a single value and then terminate, and the sequence operators in Combine are what you want to use for that kind of thing.
Combine is set up to deal with one OR many values. So you, as the consumer, need to give it some way to constrain the potentially many values into a single value, if you want to use an assign subscriber to set a value (or sink subscriber to invoke a closure where you do your save, in your case).
The sequence operators in Combine is where I'd think to look, but I can't really describe which one without knowing how many values and how you'd choose which one to apply. The two "easy" options are either first or last, but there's a variety of sequence operators that let you construct more complicated choices (including firstWhere and lastWhere which let you determine based on your own closure, which can be darned handy.
The embedded links are all to the online/free-version of Using Combine (disclosure: which I wrote) - and while I don't have any explicit examples about the sequence operators, I did flesh out the reference details for them in the book quite a bit.
Unless you're explicitly working from a publisher, you may find it easier to use a Promise library - depending on what's triggering your save. I don't know the Realm end of this to know what their API focuses on, and if you've made the publisher that's generating the data, or if that's coming from their API - and hence your desire to using Combine to solve this.

Using Just with flatMap produce Failure mismatch. Combine

I have such code
func request(request: URLRequest) -> AnyPublisher<Data, Error> {
return Just(request)
.flatMap { request in
RequestManager.request(request) // returns AnyPublisher<Data, Error>
}
.eraseToAnyPublisher()
}
and I'm getting compile error:
Instance method flatMap(maxPublishers:_:) requires the types
Just.Failure (aka Never) and Error be equivalent
And it's clear, because Just has Never as Failure and .flatMap requires Error as Failure, so Never != Error
I see 2 approaches:
using right Publisher, instead of Just, but I didn't find good candidate for this.
using some operator like .mapError, .mapError { $0 as Error }, but I'm not sure that it's great idea.
Any ideas how to handle it?
UPDATE:
it makes more sense to use
.setFailureType(to: Error.self)
or
.mapError { $0 as Error }
There is special operator setFailureType(to:). You can override failure type to whatever error type you need.
func request(request: URLRequest) -> AnyPublisher<Data, Error> {
return Just(request)
.setFailureType(to: Error.self)
.flatMap { request in
RequestManager.request(request) // returns AnyPublisher<Data, Error>
}
.eraseToAnyPublisher()
}
https://developer.apple.com/documentation/combine/just/3343941-setfailuretype
If you call .mapError() on the Just output, it will change the type to include Error, but that closure will never be called (so I wouldn’t worry) — this is what I would do unless someone has a better idea.
Normally, the Self.Error == P.Error on flatMap makes sense, as you can’t just ignore errors coming from Self. But, when Self.Error is Never, you can ignore them and produce your own errors.
While the accepted answer certainly works it's pretty verbose. I stumbled on an alternative syntax using Result<Success,Failure).publisher that is somewhat more succinct:
Result.Publisher(.success(request))
(Note that in this case I'm depending on Swift to be able to infer the error type, but you might need to declare it explicitly: Result<URLRequest, Error>.Publisher(.success(request)))

Swift 5 Result type

In Swift 5 Apple introduced Result type. It's generic enum with two cases:
public enum Result<Success, Failure: Error> {
case success(Success), failure(Failure)
}
Personally I used to two separate completions in network calls success: Completion and failure: Completion, but from what I see now, Apple pushing us to use single completion with Result type and then inside perform switch. So what are advantages of this approach with Result? Because in a lot of cases I can just omit error handling and don't write this switch. Thanks.
You shouldn’t omit cases when Result is failure. You shouldn’t do it with Result and you shouldn’t do it with your closure for failure. You should handle errors.
Anyway, Result type was introduced for simplifing completion handlers. You can have single closure for handling success or failure (primary-opinion based if two separate closures are better or not). Also Result is designed for error handling. You can simply create your own enum conforming to Error and then you can create your own error cases.
Swift 5 introduced Result<Success, Failure> associated value enumeration[About] It means that your result can be either success or failure with additional info(success result or error object). Good practice is to manage error case and success case as atomic task.
Advantages:
Result is a single/general(generic)/full type which can be used for all yes/no cases
Not optional type
Forces consumers to check all cases
public enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
To use it
//create
func foo() -> Result<String, Error> {
//logic
if ok {
return .success("Hello World")
} else {
return .failure(.someError)
}
}
//read
func bar() {
let result = foo()
switch result {
case .success(let someString):
//success logic
case .failure(let error):
//fail logic
}
}

enums with Associated Values + generics + protocol with associatedtype

I'm trying to make my API Service as generic as possible:
API Service Class
class ApiService {
func send<T>(request: RestRequest) -> T {
return request.parse()
}
}
So that the compiler can infer the response type from the request categories .auth and .data:
let apiService = ApiService()
// String
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
// Int
let intResponse = apiService.send(request: .data(.content(id: "123")))
I tried to come up with a solution using generics and a protocol with associated type to handle the parsing in a clean way. However I'm having trouble associating the request cases with the different response types in a way that it's simple and type-safe:
protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}
Endpoints
enum RestRequest {
case auth(_ request: AuthRequest)
case data(_ request: DataRequest)
// COMPILER ERROR HERE: Generic parameter 'T' is not used in function signature
func parse<T: Parseable>() -> T.ResponseType {
switch self {
case .auth(let request): return (request as T).parse()
case .data(let request): return (request as T).parse()
}
}
enum AuthRequest: Parseable {
case login(email: String, password: String)
case signupWithFacebook(token: String)
typealias ResponseType = String
func parse() -> ResponseType {
return "String!!!"
}
}
enum DataRequest: Parseable {
case content(id: String?)
case package(id: String?)
typealias ResponseType = Int
func parse() -> ResponseType {
return 16
}
}
}
How is T not used in function signature even though I'm using T.ResponseType as function return?
Is there a better still clean way to achieve this?
I'm trying to make my API Service as generic as possible:
First, and most importantly, this should never be a goal. Instead, you should start with use cases, and make sure that your API Service meets them. "As generic as possible" doesn't mean anything, and only will get you into type nightmares as you add "generic features" to things, which is not the same thing as being generally useful to many use cases. What callers require this flexibility? Start with the callers, and the protocols will follow.
func send<T>(request: RestRequest) -> T
Next, this is a very bad signature. You don't want type inference on return types. It's a nightmare to manage. Instead, the standard way to do this in Swift is:
func send<ResultType>(request: RestRequest, returning: ResultType.type) -> ResultType
By passing the expected result type as a parameter, you get rid of the type inference headaches. The headache looks like this:
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")))
How is the compiler to know that stringResponse is supposed to be a String? Nothing here says "String." So instead you have to do this:
let stringResponse: String = ...
And that's very ugly Swift. Instead you probably want (but not really):
let stringResponse = apiService.send(request: .auth(.signupWithFacebook(token: "9999999999999")),
returning: String.self)
"But not really" because there's no way to implement this well. How can send know how to translate "whatever response I get" into "an unknown type that happens to be called String?" What would that do?
protocol Parseable {
associatedtype ResponseType
func parse() -> ResponseType
}
This PAT (protocol w/ associated type) doesn't really make sense. It says something is parseable if an instance of it can return a ResponseType. But that would be a parser not "something that can be parsed."
For something that can be parsed, you want an init that can take some input and create itself. The best for that is Codable usually, but you could make your own, such as:
protocol Parseable {
init(parsing data: Data) throws
}
But I'd lean towards Codable, or just passing the parsing function (see below).
enum RestRequest {}
This is probably a bad use of enum, especially if what you're looking for is general usability. Every new RestRequest will require updating parse, which is the wrong place for this kind of code. Enums make it easy to add new "things that all instances implement" but hard to add "new kinds of instances." Structs (+ protocols) are the opposite. They make it easy to add new kinds of the protocol, but hard to add new protocol requirements. Requests, especially in a generic system, are the latter kind. You want to add new requests all the time. Enums make that hard.
Is there a better still clean way to achieve this?
It depends on what "this" is. What does your calling code look like? Where does your current system create code duplication that you want to eliminate? What are your use cases? There is no such thing as "as generic as possible." There are just systems that can adapt to use cases along axes they were prepared to handle. Different configuration axes lead to different kinds of polymorphism, and have different trade-offs.
What do you want your calling code to look like?
Just to provide an example of what this might look like, though, it'd be something like this.
final class ApiService {
let urlSession: URLSession
init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}
func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: #escaping (Response?) -> Void) {
urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
// Log your error
completion(nil)
return
}
if let data = data {
let result = try? JSONDecoder().decode(Response.self, from: data)
// Probably check for nil here and log an error
completion(result)
return
}
// Probably log an error
completion(nil)
}
}
}
This is very generic, and can apply to numerous kinds of use cases (though this particular form is very primitive). You may find it doesn't apply to all your use cases, so you'd begin to expand on it. For example, maybe you don't like using Decodable here. You want a more generic parser. That's fine, make the parser configurable:
func send<Response>(request: URLRequest,
returning: Response.Type,
parsedBy: #escaping (Data) -> Response?,
completion: #escaping (Response?) -> Void) {
urlSession.dataTask(with: request) { (data, response, error) in
if let error = error {
// Log your error
completion(nil)
return
}
if let data = data {
let result = parsedBy(data)
// Probably check for nil here and log an error
completion(result)
return
}
// Probably log an error
completion(nil)
}
}
Maybe you want both approaches. That's fine, build one on top of the other:
func send<Response: Decodable>(request: URLRequest,
returning: Response.Type,
completion: #escaping (Response?) -> Void) {
send(request: request,
returning: returning,
parsedBy: { try? JSONDecoder().decode(Response.self, from: $0) },
completion: completion)
}
If you're looking for even more on this topic, you may be interested in "Beyond Crusty" which includes a worked-out example of tying together parsers of the kind you're discussing. It's a bit dated, and Swift protocols are more powerful now, but the basic message is unchanged and the foundation of things like parsedBy in this example.