I would like to simulate async and await request from Javascript to Swift 4. I searched a lot on how to do it, and I thought I found the answer with DispatchQueue, but I don't understand how it works.
I want to do a simple stuff:
if let items = result.value {
var availableBornes = [MGLPointFeature]()
for item in items {
guard let id = item.id else { continue }
let coordinate = CLLocationCoordinate2D(latitude: Double(coor.x), longitude: Double(coor.y))
// ...
// This is an asynchronous request I want to wait
await _ = directions.calculate(options) { (waypoints, routes, error) in
guard error == nil else {
print("Error calculating directions: \(error!)")
return
}
// ...
if let route = routes?.first {
let distanceFormatter = LengthFormatter()
let formattedDistance = distanceFormatter.string(fromMeters: route.distance)
item.distance = formattedDistance
// Save feature
let feature = MGLPointFeature()
feature.attributes = [
"id": id,
"distance": formattedDistance
]
availableBornes.append(feature)
}
}
}
// This should be called after waiting for the async requests
self.addItemsToMap(availableBornes: availableBornes)
}
What should I do?
Thanks to vadian's comment, I found what I expected, and it's pretty easy. I use DispatchGroup(), group.enter(), group.leave() and group.notify(queue: .main){}.
func myFunction() {
let array = [Object]()
let group = DispatchGroup() // initialize
array.forEach { obj in
// Here is an example of an asynchronous request which use a callback
group.enter() // wait
LogoRequest.init().downloadImage(url: obj.url) { (data) in
if (data) {
group.leave() // continue the loop
}
}
}
group.notify(queue: .main) {
// do something here when loop finished
}
}
We have to await!
The async-await Swift Evolution proposal SE-0296 async/await was accepted after 2 pitches and revision modifications recently on December 24th 2020. This means that we will be able to use the feature in Swift 5.5. The reason for the delay is due to backwards-compatibility issues with Objective-C, see SE-0297 Concurrency Interoperability with Objective-C. There are many side-effects and dependencies of introducing such a major language feature, so we can only use the experimental toolchain for now. Because SE-0296 had 2 revisions, SE-0297 actually got accepted before SE-0296.
General Use
We can define an asynchronous function with the following syntax:
private func raiseHand() async -> Bool {
sleep(3)
return true
}
The idea here is to include the async keyword alongside the return type since the call site will return (BOOL here) when complete if we use the new await keyword.
To wait for the function to complete, we can use await:
let result = await raiseHand()
Synchronous/Asynchronous
Defining synchronous functions as asynchronous is ONLY forward-compatible - we cannot declare asynchronous functions as synchronous. These rules apply for function variable semantics, and also for closures when passed as parameters or as properties themselves.
var syncNonThrowing: () -> Void
var asyncNonThrowing: () async -> Void
...
asyncNonThrowing = syncNonThrowing // This is OK.
Throwing functions
The same consistency constraints are applied to throwing functions with throws in their method signature, and we can use #autoclosures as long as the function itself is async.
We can also use try variants such as try? or try! whenever we await a throwing async function, as standard Swift syntax.
rethrows unfortunately still needs to go through Proposal Review before it can be incorporated because of radical ABI differences between the async method implementation and the thinner rethrows ABI (Apple wants to delay the integration until the inefficiencies get ironed out with a separate proposal).
Networking callbacks
This is the classic use-case for async/await and is also where you would need to modify your code:
// This is an asynchronous request I want to wait
await _ = directions.calculate(options) { (waypoints, routes, error) in
Change to this:
func calculate(options: [String: Any]) async throws -> ([Waypoint], Route) {
let (data, response) = try await session.data(from: newURL)
// Parse waypoints, and route from data and response.
// If we get an error, we throw.
return (waypoints, route)
}
....
let (waypoints, routes) = try await directions.calculate(options)
// You can now essentially move the completion handler logic out of the closure and into the same scope as `.calculate(:)`
The asynchronous networking methods such as NSURLSession.dataTask now has asynchronous alternatives for async/await. However, rather than passing an error in the completion block, the async function will throw an error. Thus, we have to use try await to enable throwing behaviour. These changes are made possible because of SE-0297 since NSURLSession belongs to Foundation which is still largely Objective-C.
Code impacts
This feature really cleans up a codebase, goodbye Pyramid of Doom 👋!
As well as cleaning up the codebase, we improve error handling for nested networking callbacks since the error and result are separated.
We can use multiple await statements in succession to reduce the dependency on DispatchGroup. 👋 to Threading Deadlocks when synchronising DispatchGroups across different DispatchQueues.
Less error-prone because the API is clearer to read. Not considering all exit paths from a completions handler, and conditional branching means subtle bugs can build up that are not caught at compile time.
async / await is not back-deployable to devices running < iOS 13, so we have to add if #available(iOS 13, *) checks where supporting old devices. We still need to use GCD for older OS versions.
(Note: Swift 5 may support await as you’d expect it in ES6!)
What you want to look into is Swift's concept of "closures". These were previously known as "blocks" in Objective-C, or completion handlers.
Where the similarity in JavaScript and Swift come into play, is that both allow you to pass a "callback" function to another function, and have it execute when the long-running operation is complete. For example, this in Swift:
func longRunningOp(searchString: String, completion: (result: String) -> Void) {
// call the completion handler/callback function
completion(searchOp.result)
}
longRunningOp(searchString) {(result: String) in
// do something with result
}
would look like this in JavaScript:
var longRunningOp = function (searchString, callback) {
// call the callback
callback(err, result)
}
longRunningOp(searchString, function(err, result) {
// Do something with the result
})
There's also a few libraries out there, notably a new one by Google that translates closures into promises: https://github.com/google/promises. These might give you a little closer parity with await and async.
You can use this framework for Swift coroutines - https://github.com/belozierov/SwiftCoroutine
Unlike DispatchSemaphore, when you call await it doesn’t block the thread but only suspends coroutine, so you can use it in the main thread as well.
func awaitAPICall(_ url: URL) throws -> String? {
let future = URLSession.shared.dataTaskFuture(for: url)
let data = try future.await().data
return String(data: data, encoding: .utf8)
}
func load(url: URL) {
DispatchQueue.main.startCoroutine {
let result1 = try self.awaitAPICall(url)
let result2 = try self.awaitAPICall2(result1)
let result3 = try self.awaitAPICall3(result2)
print(result3)
}
}
In iOS 13 and up, you can now do this using Combine. Future is analogous to async and the flatMap operator on publishers (Future is a publisher) is like await. Here's an example, loosely based on your code:
Future<Feature, Error> { promise in
directions.calculate(options) { (waypoints, routes, error) in
if let error = error {
promise(.failure(error))
}
promise(.success(routes))
}
}
.flatMap { routes in
// extract feature from routes here...
feature
}
.receiveOn(DispatchQueue.main) // UI updates should run on the main queue
.sink(receiveCompletion: { completion in
// completion is either a .failure or it's a .success holding
// the extracted feature; if the process above was successful,
// you can now add feature to the map
}, receiveValue: { _ in })
.store(in: &self.cancellables)
Edit: I went into more detail in this blog post.
You can use semaphores to simulate async/await.
func makeAPICall() -> Result <String?, NetworkError> {
let path = "https://jsonplaceholder.typicode.com/todos/1"
guard let url = URL(string: path) else {
return .failure(.url)
}
var result: Result <String?, NetworkError>!
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
result = .success(String(data: data, encoding: .utf8))
} else {
result = .failure(.server)
}
semaphore.signal()
}.resume()
_ = semaphore.wait(wallTimeout: .distantFuture)
return result
}
And here is example how it works with consecutive API calls:
func load() {
DispatchQueue.global(qos: .utility).async {
let result = self.makeAPICall()
.flatMap { self.anotherAPICall($0) }
.flatMap { self.andAnotherAPICall($0) }
DispatchQueue.main.async {
switch result {
case let .success(data):
print(data)
case let .failure(error):
print(error)
}
}
}
}
Here is the article describing it in details.
And you can also use promises with PromiseKit and similar libraries
Async/await is now officially supported in Swift.
It would yield be something like this
func myFunction() async throws {
let array: [Object] = getObjects()
let images = try await withThrowingTaskGroup(of: Data.self, returning: [Data].self) { group in
array.forEach { object in
group.async {
try await LogoRequest().downloadImage(url: object.url)
}
}
return try await group.reduce([], {$0 + [$1]})
}
// at this point all `downloadImage` are done, and `images` is populated
_ = images
}
Use async/ await below like this,
enum DownloadError: Error {
case statusNotOk
case decoderError
}
Method Call
override func viewDidLoad() {
super.viewDidLoad()
async{
do {
let data = try await fetchData()
do{
let json = try JSONSerialization.jsonObject(with: data, options:.mutableLeaves)
print(json)
}catch{
print(error.localizedDescription)
}
}catch{
print(error.localizedDescription)
}
}
}
func fetchData() async throws -> Data{
let url = URL(string: "https://www.gov.uk/bank-holidays.json")!
let request = URLRequest(url:url)
let (data,response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else{
throw DownloadError.statusNotOk
}
return data
}
Here you can see a basic difference, how the callBack replacing by async/await
Basically, scene A and scene B = Call API by the closure.
Scene C and scene D= Calling API by Async/Await.
Scene E = Serial API call by nested closure.
Scene F= Serial API call by Async/Await.
Scene G = Parallel API call by Async/Await.
Parallel API call
Related
I was trying to wrap Alamofire with async, but the answers that I found couldn't help me, but after finding the solution, I thought that I should help some one with this same problem.
There's no need to wrap Alamofire for async / await, it already offers a native version of that API.
let response = await AF.request(...).serializingDecodable(<YourType>.self)
You can also await .result and try await .value on the above to access the parts you need.
Following a very decoupled MVVM project, my DataLayer was outside from the ViewModel Layer, so I couldn't let the AF.request handler the Diffable direct. So my solution was wraping the #escaping with withCheckedContinuation
In DataLayer.swift:
func fetchRequests() async -> [MyModel] {
await withCheckedContinuation{ continuation in
downloadJson{ models in
continuation.resume(returning: models)
}
}
}
private func downloadJson(completion: #escaping ([MyModel]) -> Void){
let url = "https://......"
AF.request(url).responseDecodable(of: [MyModel].self){ response in
guard let models = response.value else {return}
completion(models)
}
}
ModelModule.swift:
var data: [MyModel]
func loadData() async {
data = await dataLayer.fetchRequests()
}
Finally the ViewModel.swift:
func configure(dataSource: AnyDiffableDataSource<MySection, MyModelItem>) {
self.dataSource = dataSource
Task {
await modelModule.loadData()
dataSource.create(sections: [.main], animated: true)
updateDateSource(animated: true)
//... rest of the configuration
}
}
I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}
I am trying to pass the value of gyroX to another function but it just ends up in it having a value of 0 when I use it as gyroX in that other function.
Here is the code:
var gyroX = Float()
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
gyroX = Float(myData.rotationRate.x)
}
}
With Xcode 13 Beta and Swift 5.5
This is a problem that we can now solve with Async/Await's Continuations
We would first make a function that converts the callback into an awaitable result like:
func getXRotation(from motion: CMMotionManager) async throws -> Float {
try await withCheckedThrowingContinuation { continuation in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
continuation.resume(returning: Float(myData.rotationRate.x))
} else {
throw GyroUpdateFailure()
}
}
}
}
Then we can assign the variable and use it like so:
let gyroX = try await getXRotation(from: motion)
callSomeOtherFunction(with: gyroX)
With Xcode <= 12 and Combine
In the current release of Swift and Xcode we can use the Combine framework to make callback handling a little easier for us. First we'll convert the closure from the motion manager into a "Future". Then we can use that future in a combine chain.
func getXRotation(from motion: CMMotionManager) -> Future<CMGyroData, Error> {
Future { promise in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
promise(.success(myData))
} else {
promise(.failure(GyroUpdateFailure()))
}
}
}
}
// This is the other function you want to call
func someOtherFunction(_ x: Float) {}
// Then we can use it like so
_ = getXRotation(from: motion)
.eraseToAnyPublisher()
.map { Float($0.rotationRate.x) }
.map(someOtherFunction)
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: break
}
} receiveValue: {
print($0)
}
There are some important parts to the combine flow. The _ = is one of them. The result of "sinking" on a publisher is a "cancellable" object. If we don't store that in a local variable the system can clean up the task before it fishes executing. So you will want to do that for sure.
I highly recommend you checkout SwiftBySundell.com to learn more about Combine or Async/Await and RayWenderlich.com for mobile development in general.
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.