Swift Optimise nested async calls - ios

I have this case where I need to make 3 nested async calls to receive the data I want.
So the second call needs data from the first one and the third one needs data from the second one. I do not have a lot of cases like this. Only this one and another one with only two nested call so I was thinking about a pure swift solution without any external libraries but I'm open to everything.
Since I'm using Firebase, is it better to move this logic to CloudFunctions? So to prepare it in the backend?
FirestoreService().fetchCollection(query: query) { (result: Result<[Request], Error>) in
// do stuff
FirestoreService().fetchCollection(query: query) { (result: Result<[Request], Error>) in
// do stuff
FirestoreService().fetchDocument(documentReference: documentReference) { (result: Result<Package, Error>) in
// finish
}
}
}
}

If you don't to used 3rd party library, then probably you want to consider wrap those operations inside some class, and utilise closure in imperative way.
here is the sample:
class CustomFirestoreHandler {
private var onFetchFirstQueryArrived: ((Result<[Request], Error>) -> ())? = nil
private var onFetchSecondQueryArrived: ((Result<[Request], Error>) -> ())? = nil
private var onFetchDocumentArrived: ((Result<Package, Error>) -> ())? = nil
init() {
onFetchFirstQueryArrived = { [weak self] (result: Result<[Request], Error>) in
self?.executeSecondQuery()
}
onFetchSecondQueryArrived = { [weak self] (result: Result<[Request], Error>) in
self?.executeFetchDocument()
}
}
func executeQuery(completion: #escaping (Result<Package, Error>) -> ()) {
self.onFetchDocumentArrived = completion
FirestoreService().fetchCollection(query: query) { [weak self] (result: Result<[Request], Error>) in
// validate if some error occurred and do early return here, so that we don't need necessarily call second query.
if (result.error == whatever) {
self?.onFetchDocumentArrived?(result)
return
}
self?.onFetchFirstQueryArrived?(result)
}
}
private func executeSecondQuery() {
FirestoreService().fetchCollection(query: query) { [weak self] (result: Result<[Request], Error>) in
// validate if some error occurred and do early return here, so that we don't need necessarily call fetch document.
if (result.error == whatever) {
self?.onFetchDocumentArrived?(result)
return
}
self?.onFetchSecondQueryArrived?(result)
}
}
private func executeFetchDocument() {
FirestoreService().fetchDocument(documentReference: documentReference) { (result: Result<Package, Error>) in
self?.onFetchDocumentArrived?(result)
}
}
}
And here's the usage of CustomFirestoreHandler above :
let firestoreHandler = CustomFirestoreHandler()
firestoreHandler.executeQuery { (result: Result<Package, Error>) in
// Handle `result` here...
}
I know it look complicated, but this is the only way I think (CMIIW) at the moment to prevent pyramid of dooms since swift doesn't have async await style(just like javascript does).

Related

API funtion using RxSwift

I want to change the API request code written using the closure to RxSwift.
For example, I would like to make rxGetList() function using getList() function.
// This function cannot be modified.
func getList(success: #escaping ([String]) -> Void,
failure: #escaping (Error) -> Void) {
// Request to Server...
}
func rxGetList() -> Observable<String> {
// Using getList() function
// TODO
}
What code should I write in TODO section?
Please give me some advice.
The easiest way to meet your expectations is to use something like this:
func rxGetList() -> Observable<String> {
return Observable.create { observer in
getList(success: { result in
for everyString in result {
observer.onNext(everyString)
}
observer.onCompleted()
}, failure: { error in
observer.onError(error)
})
return Disposables.create() {
// specify any action to be performed on observable dispose (like cancel URL task)
}
}
}
Note that you have [String] specified as an input type of your success closure. If it's not a typo then above code fits. If you want one String instead, it's as simple as this:
func rxGetList() -> Observable<String> {
return Observable.create { observer in
getList(success: { result in
observer.onNext(result)
observer.onCompleted()
}, failure: { error in
observer.onError(error)
})
return Disposables.create() {
// specify any action to be performed on observable dispose (like cancel URL task)
}
}
}
Petr Grigorev's answer is the correct one, but if you want to have fun with some extreme function composition, here's a more advanced way to handle it:
let rxGetList = Observable.create(rx_(getList(success:failure:)))
.flatMap { Observable.from($0) }
func rx_<A>(_ fn: #escaping (#escaping (A) -> Void, #escaping (Error) -> Void) -> Void) -> (AnyObserver<A>) -> Disposable {
{
fn(singleObserve($0), $0.onError)
return Disposables.create()
}
}
func singleObserve<A>(_ observer: AnyObserver<A>) -> (A) -> Void {
{
observer.onNext($0)
observer.onCompleted()
}
}
I'm not sure about actually using the above, but if you have a lot of functions that you want to wrap, it may help reduce the amount of code you have to write.

OperationQueue / DispatchGroup and recursion

I have a problem understanding how to use GCD when using asynchronous, recursive calls to the API.
Below is one of the three similar methods that contains the same logic, just for different data and API endpoint. If there is no next page request the method should finish and next method should start.
How would I make sure that fetchItems2 gets called after fetchItems1 finishes, and fetchItems3 after fetchItems2?
private func fetchItems1(completion: #escaping (Error?) -> Void) {
var _items = [Item]()
func handleReceivedItemsPage(_ page: PagingObject<Item>, _completion: ((Error?) -> Void)?) {
let newItems = page.items!
_tracks.append(contentsOf: newTracks)
if page.canMakeNextRequest {
page.getNext(success: { nextPage in
handleReceivedItemsPage(nextPage)
}) { nextError in
_completion?(nextError)
}
} else {
// Finished, next method can now start
self.items = _items
_completion?(nil)
}
}
API.getSavedItems(success: { page in
handleReceivedItemsPage(page, _completion: completion)
}, failure: completion)
}
private func fetchItems2(completion: #escaping (Error?) -> Void)) { ... }
private func fetchItems3(completion: #escaping (Error?) -> Void)) { ... }
You can keep an extra variable that keeps track of when the API calls are complete. In the completion block, increment this variable. Then, when the variable reaches the amount of API calls complete, perform your task.
I would use DispatchGroup:
public void FetchItems(completion: #escaping (Error?) -> Void) {
let group = DispatchGroup()
group.enter()
fetchItems1() { error in
completion(error)
group.leave()
}
group.wait()
group.enter()
fetchItems2() { error in
completion(error)
group.leave()
}
// 3rd function call
}
Code after group.wait() is not called until the number of group.enter() and group.leave() invocations is equal.

What should I include in my network mocking function?

I want to test my network request code. Testing is not my strong point, and I'm doing this mainly to get better at creating testable code. I usually use Alamofire, but want to bypass that for unit tests. I have created a protocol like this:
protocol NetworkSession {
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()
)
}
I conform to this protocol in my class NetworkSessionManager:
final class NetworkSessionManager : NetworkSession {
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()) {
guard let url = endpoint.url else {
return completion(.failure(.couldNotEncode))
}
Alamofire.request(url, method: method, headers: headers)
.validate()
.responseJSON { response in
completion(.success(response))
}
}
}
}
Then in my BackendClient class I inject and call my NetworkSessionManager like this:
private let session: NetworkSession
init(session: NetworkSession = NetworkSessionManager()) {
self.session = session
}
func someFunction(...) {
session.request(...)
}
Now I know I need to create a mock class that conforms to NetworkSession, but I'm not sure what I should do in it? How do I test all the possibilities and returned data? There are different end points on my server that return different types of data. If someone could point me towards the next steps I should take, I'd be very grateful.
final class NetworkSessionManagerMock : NetworkSession {
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()) {
???
}
}
I'll show two examples that can hopefully give you something to start with in mocking out your NetworkSession.
First though, you can add additional properties to your mock class that support injecting values to test various cases. I will demonstrate an error situation and a return for now.
enum MyError: Swift.Error {
case failed
}
final class NetworkSessionManagerMock : NetworkSession {
var myError: MyError?
var dataResponse: Any? // This should hopefully be typed to something
func request(
_ endpoint: Endpoint,
method: HTTPMethod,
completion: #escaping (Result<<DataResponse<Any>>, RequestError>) -> ()) { [weak self] in
if let myError = self?.myError {
// Send error with completion handler
} else if let dataResponse = self?.dataResponse {
// Send data response
}
}
}
The idea here is that you instantiate the mock in your test, then set the variables as needed to test the respective part. Your mock will then proceed normally.
I did not test this code for syntax correctness though, so let me know if there is a mistake in it.

Firebase observer architecture

Okay, so I'm trying to build an iOS app that relies on Firebase (To work with its android version)
I started with creating a repository for each actor in my app and a general repository to manage them all
Each repository manages the observers of this actor. An example:
Inside the PagesRepository, this is a function that retrieves all the pages from Firebase and returns it inside a completionHandler:
//MARK: Gets the whole pages list
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observe(DataEventType.value) { pagesSnapshot in
guard pagesSnapshot.exists() else {
displayError(error: "Pages snapshot doesn't exist")
return
}
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}
}
And then I call it from the ViewController like this:
repository.getPagesList { (pages, error) in
guard error == nil else {
return
}
//Do processing
}
I know this may be a lot to take in, but my problem is that every time I call the function, it creates a new observer but doesn't cancel the old one... So, the completionHandler is called multiple times with different values
How should I manage this problem?
(Sorry for being complicated and a little unclear, I'm just really lost)
It seems like you only want to observe the value once so I would use this instead:
func getPagesList(completionHandler: #escaping (_ pages: [Page]?, _ error: NSError?) -> Void) {
func displayError(error: String) {
print(error)
completionHandler(nil, self.getErrorFromString(error))
}
pagesRef.observeSingleEvent(of: .value, with: { (pagesSnapshot) in
var pagesList = [Page]()
for pageSnapshot in pagesSnapshot.children {
pagesList.append(Page(snapshot: pageSnapshot as! DataSnapshot))
}
completionHandler(pagesList, nil)
}) { (error) in
// Display error
}
}

How to use typealias when getting data from server

I am trying to get user data from a server. The application does not have to show any views until the data is loaded.
I read about typealias and I don't understand how to use it.
What I want: when data is loaded, move on to next step. If failed, load data again.
Here's how I declare typealias
typealias onCompleted = () -> ()
typealias onFailed = () -> ()
Here is my request code
func getUserData(_ completed: #escaping onCompleted, failed: #escaping onFailed){
let fullURL = AFUtils.getFullURL(AUTHURL.getUserData)
AFNetworking.requestGETURL(fullURL, params: nil, success: {
(JSONResponse) -> Void in
if let status = JSONResponse["status"].string {
switch status{
case Status.ok:
completed()
break
default:
failed()
break
}
}
})
}
But how could I use this on my view controller when calling getUserData?
Assuming your custom AFNetworking.requestGETURLs completion handler is called on the main queue:
func viewDidLoad() {
super.viewDidLoad()
getUserData({
//do somthing and update ui
}) {
//handle error
}
}
Edit:
How I understand your comment, you actually want to name your completion and error block parameters. If so, change the method to :
func getUserData(completion completed: #escaping onCompleted, error failed: #escaping onFailed){ ... }
and call it like this:
getUserData(completion: {
//do somthing and update ui
}, error: {
//handle error
})

Resources