I'm exploring Combine Swift with this project https://github.com/sgl0v/TMDB
and I'm trying to replace its imageLoader with something that supports Combine: https://github.com/JanGorman/MapleBacon
The project has a function that returns the type AnyPublisher<UIImage?, Never>.
But the imageLoader MapleBacon library returns the type AnyPublisher<UIImage, Error>.
So I'm trying to convert types with this function:
func convert(_ loader: AnyPublisher<UIImage, Error>) -> AnyPublisher<UIImage?, Never> {
// here.
}
I actually found a question that is kinda similar to mine, but the answers weren't helpful:
https://stackoverflow.com/a/58234908/3231194
What I've tried to so far (Matt's answer to the linked question).
The sample project has this function:
func loadImage(for movie: Movie, size: ImageSize) -> AnyPublisher<UIImage?, Never> {
return Deferred { return Just(movie.poster) }
.flatMap({ poster -> AnyPublisher<UIImage?, Never> in
guard let poster = movie.poster else { return .just(nil) }
let url = size.url.appendingPathComponent(poster)
let a = MapleBacon.shared.image(with: url)
.replaceError(with: UIImage(named: "")!) // <----
})
.subscribe(on: Scheduler.backgroundWorkScheduler)
.receive(on: Scheduler.mainScheduler)
.share()
.eraseToAnyPublisher()
}
if I do replaceError,
I get the type Publishers.ReplaceError<AnyPublisher<UIImage, Error>>
BUT, I was able to solve this one, by extending the library.
extension MapleBacon {
public func image(with url: URL, imageTransformer: ImageTransforming? = nil) -> AnyPublisher<UIImage?, Never> {
Future { resolve in
self.image(with: url, imageTransformer: imageTransformer) { result in
switch result {
case .success(let image):
resolve(.success(image))
case .failure:
resolve(.success(UIImage(named: "")))
}
}
}
.eraseToAnyPublisher()
}
}
First, you need to map a UIImage to a UIImage?. The sensible way to do this is of course to wrap each element in an optional.
Then, you try to turn a publisher that sometimes produces errors to a publisher that Never produces errors. You replaceError(with:) an element of your choice. What element should you replace errors with? The natural answer, since your publisher now publishes optional images, is nil! Of course, assertNoFailure works syntactically too, but you might be downloading an image here, so errors are very likely to happen...
Finally, we need to turn this into an AnyPublisher by doing eraseToAnyPublisher
MapleBacon.shared.image(with: url)
.map(Optional.some)
.replaceError(with: nil)
.eraseToAnyPublisher()
Related
Dear Advanced Programmers,
Could you please assist a fairly new programmer into editing this code. Seems that the new version of Xcode does not support the below code and displays the error:
**"Contextual closure type '(Directions.Session, Result<RouteResponse, DirectionsError>) -> Void' (aka '((options: DirectionsOptions, credentials: DirectionsCredentials), Result<RouteResponse, DirectionsError>) -> ()') expects 2 arguments, but 3 were used in closure body"**
The code is copied directly from the mapbox documentation website. Any form of assistance would be appreciated. Thanks in advance.
func getRoute(from origin: CLLocationCoordinate2D,
to destination: MGLPointFeature) -> [CLLocationCoordinate2D]{
var routeCoordinates : [CLLocationCoordinate2D] = []
let originWaypoint = Waypoint(coordinate: origin)
let destinationWaypoint = Waypoint(coordinate: destination.coordinate)
let options = RouteOptions(waypoints: [originWaypoint, destinationWaypoint], profileIdentifier: .automobileAvoidingTraffic)
_ = Directions.shared.calculate(options) { (waypoints, routes, error) in
guard error == nil else {
print("Error calculating directions: \(error!)")
return
}
guard let route = routes?.first else { return }
routeCoordinates = route.coordinates!
self.featuresWithRoute[self.getKeyForFeature(feature: destination)] = (destination, routeCoordinates)
}
return routeCoordinates
}
Please, developers, learn to read error messages and/or the documentation.
The error clearly says that the type of the closure is (Directions.Session, Result<RouteResponse, DirectionsError>) -> Void (2 parameters) which represents a session (a tuple) and a custom Result type containing the response and the potential error.
You have to write something like
_ = Directions.shared.calculate(options) { (session, result) in
switch result {
case .failure(let error): print(error)
case .success(let response):
guard let route = response.routes?.first else { return }
routeCoordinates = route.coordinates!
self.featuresWithRoute[self.getKeyForFeature(feature: destination)] = (destination, routeCoordinates)
}
}
Apart from the issue it's impossible to return something from an asynchronous task, you have to add a completion handler for example
func getRoute(from origin: CLLocationCoordinate2D,
to destination: MGLPointFeature,
completion: #escaping ([CLLocationCoordinate2D]) -> Void) {
and call completion(route.coordinates!) at the end of the success case inside the closure
I'm using Combine and it happens to me many times that I have the need to emit Publishers with single values.
For example when I use flat map and I have to return a Publisher with a single value as an error or a single object I use this code, and it works very well:
return AnyPublisher<Data, StoreError>.init(
Result<Data, StoreError>.Publisher(.cantDownloadProfileImage)
)
This creates an AnyPublisher of type <Data, StoreError> and emits an error, in this case: .cantDownloadProfileImage
Here a full example how may usages of this chunk of code.
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let urlString = user.imageURL,
let url = URL(string: urlString)
else {
return AnyPublisher<UIImage?, StoreError>
.init(Result<UIImage?, StoreError>
.Publisher(nil))
}
return NetworkService.getData(url: url)
.catch({ (_) -> AnyPublisher<Data, StoreError> in
return AnyPublisher<Data, StoreError>
.init(Result<Data, StoreError>
.Publisher(.cantDownloadProfileImage))
})
.flatMap { data -> AnyPublisher<UIImage?, StoreError> in
guard let image = UIImage(data: data) else {
return AnyPublisher<UIImage?, StoreError>
.init(Result<UIImage?, StoreError>.Publisher(.cantDownloadProfileImage))
}
return AnyPublisher<UIImage?, StoreError>
.init(Result<UIImage?, StoreError>.Publisher(image))
}
.eraseToAnyPublisher()
}
Is there an easier and shorter way to create an AnyPublisher with a single value inside?
I think I should use the Just() object in somehow, but I can't understand how, because the documentation at this stage is very unclear.
The main thing we can do to tighten up your code is to use .eraseToAnyPublisher() instead of AnyPublisher.init everywhere. This is the only real nitpick I have with your code. Using AnyPublisher.init is not idiomatic, and is confusing because it adds an extra layer of nested parentheses.
Aside from that, we can do a few more things. Note that what you wrote (aside from not using .eraseToAnyPublisher() appropriately) is fine, especially for an early version. The following suggestions are things I would do after I have gotten a more verbose version past the compiler.
We can use Optional's flatMap method to transform user.imageURL into a URL. We can also let Swift infer the Result type parameters, because we're using Result in a return statement so Swift knows the expected types. Hence:
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let url = user.imageURL.flatMap({ URL(string: $0) }) else {
return Result.Publisher(nil).eraseToAnyPublisher()
}
We can use mapError instead of catch. The catch operator is general: you can return any Publisher from it as long as the Success type matches. But in your case, you're just discarding the incoming failure and returning a constant failure, so mapError is simpler:
return NetworkService.getData(url: url)
.mapError { _ in .cantDownloadProfileImage }
We can use the dot shortcut here because this is part of the return statement. Because it's part of the return statement, Swift deduces that the mapError transform must return a StoreError. So it knows where to look for the meaning of .cantDownloadProfileImage.
The flatMap operator requires the transform to return a fixed Publisher type, but it doesn't have to return AnyPublisher. Because you are using Result<UIImage?, StoreError>.Publisher in all paths out of flatMap, you don't need to wrap them in AnyPublisher. In fact, we don't need to specify the return type of the transform at all if we change the transform to use Optional's map method instead of a guard statement:
.flatMap({ data in
UIImage(data: data)
.map { Result.Publisher($0) }
?? Result.Publisher(.cantDownloadProfileImage)
})
.eraseToAnyPublisher()
Again, this is part of the return statement. That means Swift can deduce the Output and Failure types of the Result.Publisher for us.
Also note that I put parentheses around the transform closure because doing so makes Xcode indent the close brace properly, to line up with .flatMap. If you don't wrap the closure in parens, Xcode lines up the close brace with the return keyword instead. Ugh.
Here it is all together:
func downloadUserProfilePhoto(user: User) -> AnyPublisher<UIImage?, StoreError> {
guard let url = user.imageURL.flatMap({ URL(string: $0) }) else {
return Result.Publisher(nil).eraseToAnyPublisher()
}
return NetworkService.getData(url: url)
.mapError { _ in .cantDownloadProfileImage }
.flatMap({ data in
UIImage(data: data)
.map { Result.Publisher($0) }
?? Result.Publisher(.cantDownloadProfileImage)
})
.eraseToAnyPublisher()
}
import Foundation
import Combine
enum AnyError<O>: Error {
case forcedError(O)
}
extension Publisher where Failure == Never {
public var limitedToSingleResponse: AnyPublisher<Output, Never> {
self.tryMap {
throw AnyError.forcedError($0)
}.catch { error -> AnyPublisher<Output, Never> in
guard let anyError = error as? AnyError<Output> else {
preconditionFailure("only these errors are expected")
}
switch anyError {
case let .forcedError(publishedValue):
return Just(publishedValue).eraseToAnyPublisher()
}
}.eraseToAnyPublisher()
}
}
let unendingPublisher = PassthroughSubject<Int, Never>()
let singleResultPublisher = unendingPublisher.limitedToSingleResponse
let subscription = singleResultPublisher.sink(receiveCompletion: { _ in
print("subscription ended")
}, receiveValue: {
print($0)
})
unendingPublisher.send(5)
In the snippet above I am converting a passthroughsubject publisher which can send a stream of values into something that stops after sending the first value. The essence of the snippet in based on the WWDC session about introduction to combine https://developer.apple.com/videos/play/wwdc2019/721/ here.
We are esentially force throwing an error in tryMap and then catching it with a resolving publisher using Just which as the question states will finish after the first value is subscribed to.
Ideally the demand is better indicated by the subscriber.
Another slightly more quirky alternative is to use the first operator on a publisher
let subscription_with_first = unendingPublisher.first().sink(receiveCompletion: { _ in
print("subscription with first ended")
}, receiveValue: {
print($0)
})
I am working with a web API which delivers results up to a given limit (pageSize parameter of the request). If the number of results surpasses this limit, the response message is pre-populated with an URL to which the follow-up request can be made to fetch more results. If there are even more results, this is again indicated in the same manner.
My intend is to fetch all results at once.
Currently I have something like the following request and response structures:
// Request structure
struct TvShowsSearchRequest {
let q: String
let pageSize: Int?
}
// Response structure
struct TvShowsSearchResponse: Decodable {
let next: String?
let total : Int
let searchTerm : String
let searchResultListShow: [SearchResult]?
}
When resolving the problem 'old style' using completion handlers, I had to write a handler, which is triggering a 'handle more' request with the URL of the response:
func handleResponse(request: TvShowsSearchRequest, result: Result<TvShowsSearchResponse, Error>) -> Void {
switch result {
case .failure(let error):
fatalError(error.localizedDescription)
case .success(let value):
print("> Total number of shows matching the query: \(value.total)")
print("> Number of shows fetched: \(value.searchResultListShow?.count ?? 0)")
if let moreUrl = value.next {
print("> URL to fetch more entries \(moreUrl)")
// start recursion here: a new request, calling the same completion handler...
dataProvider.handleMore(request, nextUrl: moreUrl, completion: handleResponse)
}
}
}
let request = TvShowsSearchRequest(query: "A", pageSize: 50)
dataProvider.send(request, completion: handleResponse)
Internally the send and handleMore functions are both calling the same internalSend which is taking the request and the url, to call afterwards URLSession.dataTask(...), check for HTTP errors, decode the response and call the completion block.
Now I want to use the Combine framework and use a Publisher which is providing the paged responses automatically, without the need to call for another Publisher.
I have therefore written a requestPublisher function which takes request and the (initial) url and returns a URLSession.dataTaskPublisher which checks for HTTP errors (using tryMap), decode the response.
Now I have to ensure that the Publisher automatically "renews" itself whenever the last emitted value had a valid next URL and the completion event occurs.
I've found that there is a Publisher.append method which would exactly do this, but the problem I had so far: I want to append only under a certain condition (=valid next).
The following pseudo-code illustrates it:
func requestPublisher(for request: TvShowsSearchRequest, with url: URL) -> AnyPublisher<TvShowsSearchResponse, Error> {
// ... build urlRequest, skipped here ...
let apiCall = self.session.dataTaskPublisher(for: urlRequest)
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.server(message: "No HTTP response received")
}
if !(200...299).contains(httpResponse.statusCode) {
throw APIError.server(message: "Server respondend with status: \(httpResponse.statusCode)")
}
return data
}
.decode(type: TvShowsSearchResponse.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
return apiCall
}
// Here I'm trying to use the Combine approach
var moreURL : String?
dataProvider.requestPublisher(request)
.handleEvents(receiveOutput: {
moreURL = $0.next // remember the "next" to fetch more data
})
.append(dataProvider.requestPublisher(request, next: moreURL)) // this does not work, because moreUrl was not prepared at the time of creation!!
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
.store(in: &cancellableSet)
I suppose there are people out there who have already resolved this problem in a reactive way. Whenever I find a doable solution, it involves again recursion. I don't think this is how a proper solution should look like.
I'm looking for a Publisher which is sending the responses, without me providing a callback function. Probably there must be a solution using Publisher of Publishers, but I'm not yet understanding it.
After the comment of #kishanvekariya I've tried to build everything with multiple publishers:
The mainRequest publisher which is getting the response to the "main" request.
A new urlPublisher which is receiving all the next URLs of the "main" or any follow-up requests.
A new moreRequest publisher which is fetching for each value of urlPublisher a new request, sending all next URLs back to the urlPublisher.
Then I tried to attach the moreRequest publisher to the mainRequest with append.
var urlPublisher = PassthroughSubject<String, Error>()
var moreRequest = urlPublisher
.flatMap {
return dataProvider.requestPublisher(request, next: $0)
.handleEvents(receiveOutput: {
if let moreURL = $0.next {
urlPublisher.send(moreURL)
}
})
}
var mainRequest = dataProvider.requestPublisher(request)
.handleEvents(receiveOutput: {
if let moreURL = $0.next {
urlPublisher.send(moreURL)
}
})
.append(moreRequest)
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
.store(in: &cancellableSet)
But this still does not work... I always get the result of the "main" request. All follow up requests are missing.
It seems that I've found the solution myself.
The idea is, that I have an urlPublisher which is initialized with the first URL which is then executed and may feed a next URL to the urlPublisher and by doing so causing a follow-up request.
let url = endpoint(for: request) // initial URL
let urlPublisher = CurrentValueSubject<URL, Error>(url)
urlPublisher
.flatMap {
return dataProvider.requestPublisher(for: request, with: $0)
.handleEvents(receiveOutput: {
if let next = $0.next, let moreURL = URL(string: self.transformNextUrl(nextUrl: next)) {
urlPublisher.send(moreURL)
} else {
urlPublisher.send(completion: .finished)
}
})
}
.sink(receiveCompletion: { print($0) }, receiveValue: { print($0) })
.store(in: &cancellableSet)
So in the end, I used composition of two publishers and flatMap instead of the non-functional append. Probably this is also the solution one would target from the start...
Before I dive into the response, just wanted to say that requesting all pages at once might not be the best idea:
it adds stress on the server, likely the paged API is there for a reason, to avoid costly operations on the backend
there is always a discussion on what to do when a page request fails: you report error, you report the partial results, you retry the request?
keep in mind that once you launch your product and have many clients requests for the whole data set of TV shows, the backend server might become overloaded, and generating even more failures
Now, back to the business, assuming your requestPublisher is properly working, what you can do is to write a publisher that chains those calls, and doesn't report values until the last page was received.
The code might look like this:
func allPages(for request: TvShowsSearchRequest, with url: URL) -> AnyPublisher<TvShowsSearchResponse, Error> {
// helper function to chain requests for all pages
func doRequest(with pageURL: URL, accumulator: TvShowsSearchResponse) -> AnyPublisher<TvShowsSearchResponse, Error> {
requestPublisher(for: request, with: pageURL)
.flatMap { (r: TvShowsSearchResponse) -> AnyPublisher<TvShowsSearchResponse, Error> in
if let next = r.next, let nextURL = URL(string: next) {
// we have a `next` url, append the received page,
// and make the next request
return doRequest(with: nextURL, accumulator: accumulator.accumulating(from: r))
} else {
// no more pages, we have the response already build up
// just report it
return Just(accumulator).setFailureType(to: Error.self).eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
return doRequest(with: url, accumulator: TvShowsSearchResponse())
}
You basically use a TvShowsSearchResponse as an accumulator for the results of the chained request.
The above code also needs the following extension:
extension TvShowsSearchResponse {
init() {
self.init(next: nil, total: 0, searchTerm: "", searchResultListShow: nil)
}
func accumulating(from other: TvShowsSearchResponse) -> TvShowsSearchResponse {
TvShowsSearchResponse(
next: nil,
total: other.total,
searchTerm: other.searchTerm,
searchResultListShow: (searchResultListShow ?? []) + (other.searchResultListShow ?? []))
}
}
, as for clarity the code that accumulates the values of searchResultListShow was placed into a dedicated extension.
Introduction:
I'm introducing a Result framework (antitypical) in some points of my app. In example, given this function:
func findItem(byId: Int, completion: (Item?,Error?) -> ());
foo.findItem(byId: 1) { item, error in
guard let item = item else {
// Error case
handleError(error!)
return;
}
// Success case
handleSuccess(item)
}
I implement it this way with Result:
func findItem(byId: Int, completion: Result<Item,Error>) -> ());
foo.findItem(byId: 1) { result in
swith result {
case let success(item):
// Success case
handleSuccess(item)
case let failure(error):
// Error case
handleError(error!)
}
}
Question
What is the correct way of implementing a result where the success case returns nothing?. Something like:
func deleteItem(byId: Int, completion: (Error?) -> ());
foo.deleteItem(byId: 1) { error in
if let error = error {
// Error case
handleError(error)
return;
}
// Success case
handleSuccess()
}
In java I would implement a Result whats the correct way to do this in Swift
The best way is exactly what you've done: Error? where nil indicates success. It's quite clear and simple.
That said, another answer (and one that I've used) is exactly in your question: "How to handle Void success case with Result." The success case passes Void, so pass Void:
Result<Void, Error>
"Void" doesn't mean "returns nothing." It's a type in Swift, a type that has exactly one value: the empty tuple (). That also happens to be the type:
public typealias Void = ()
As a matter of convention, we use Void to mean the type, and () to mean the value. The one thing that's a bit strange about using Void this way in a Result is the syntax. You wind up with something like:
return .success(())
The double-parentheses are a little ugly and slightly confusing. So even though this is nicely parallel to other Result-using code, I typically just use Error? in this case. If I had a lot of it, though, I'd consider creating a new type for it:
enum VoidResult {
case .success
case .failure(Error)
}
You can add this extension, to simplify your life.
public extension Result where Success == Void {
/// A success, storing a Success value.
///
/// Instead of `.success(())`, now `.success`
static var success: Result {
return .success(())
}
}
// Now
return .success
Gists
I found Rob's answer really interesting and smart. I just want to contribute with a possible working solution to help others:
enum VoidResult {
case success
case failure(Error)
}
/// Performs a request that expects no data back but its success depends on the result code
/// - Parameters:
/// - urlRequest: Url request with the request config
/// - httpMethodType: HTTP method to be used: GET, POST ...
/// - params: Parameters to be included with the request
/// - headers: Headers to be included with the request
/// - completion: Callback trigered upon completion
func makeRequest(url: URL,
httpMethodType: HTTPMethodType,
params: [String:Any],
headers: [String:String],
completion: #escaping (VoidResult) -> Void){
let alamofireHTTPMethod = httpMethodType.toAlamofireHTTPMethod()
let parameterEncoder: ParameterEncoding
switch alamofireHTTPMethod {
case .get:
parameterEncoder = URLEncoding.default
case .post:
parameterEncoder = JSONEncoding.default
default:
parameterEncoder = URLEncoding.default
}
Log.d(message: "Calling: \(url.absoluteString)")
AF.request(url,
method: alamofireHTTPMethod,
parameters: params,
encoding:parameterEncoder,
headers: HTTPHeaders(headers)).response { response in
guard let statusCode = response.response?.statusCode,
(200 ..< 300) ~= statusCode else {
completion(.failure(NetworkFetcherError.networkError))
return
}
completion(.success)
}
}
Try this
Note this is example you can change as per your test
typealias resultHandler = (_ responseItems: AnyObject, _ error: Error) -> Void
func deleteItem(byId: Int, completion: resultHandler){
completion(Items, error)
}
Calling
self.deleteItem(byId: 1) { (result, error) in
if error ==nil{
}
}
This is a question about the objc.io video about networking.
Note: I have seen similar questions, but none of the suggestions worked
for me.
In the sample code, the main call is:
Webservice().load(resource: Episode.all) { result in
print(result!)
}
and the load function looks like this:
func load<A>(resource: Resource<A>, completion: #escaping (A?) -> ()) {
URLSession.shared.dataTask(with: resource.url as URL) { data, _, _ in
guard let data = data else {
completion(nil)
return
}
completion(resource.parse(data as NSData))
}.resume()
}
For simplicity they just print the result (an array of dictionaries), but I don't understand how I can further use it, because it is inside the closure. This way I can eg show the array in a UITableView or something like that.
So what I would like to do is something like:
var episodes = [Episode]()
Webservice().load(resource: Episode) { result in
episodes = result!
}
print(episodes)
But this results in an empty array being printed.
I also tried using
DispatchQueue.main.async {
episodes = result!
}
But again, nothing is printed.
How can I accomplish that?