DispatchGroup returning multiple times - ios

Below code which I am using to make concurrent API call. somehow this method returning multiple times. I have tested without DispatchGroup, It is working as expected. Help me to find why it is calling multiple times.
My Code Snippet :
func makeConcurrentCallForUpdating(_ parent: Parent,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void)
let fetchGroup = DispatchGroup()
let queue = DispatchQueue.global(qos: .default)
let endPoints = [.email, .others ]
DispatchQueue.concurrentPerform(iterations: endPoints.count) { (index) in
let enumType = endPoints[index]
switch enumType {
case .email:
updateEmail(parent, fetchGroup: fetchGroup, completionBlock: completionBlock)
case .others:
update(parent, fetchGroup: fetchGroup, completionBlock: completionBlock)
default:
break
}
}
fetchGroup.notify(queue: queue) {
if self.endPoints.count > 0 {
completionBlock(false, error)
} else {
self.saveUpdated(parent, completionBlock: completionBlock)
}
}
}
#MARK: EMAIL CALL
fileprivate func updateEmail(_ parent: Parent,
fetchGroup: DispatchGroup,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void) {
fetchGroup.enter()
updateEmail(parent: parent) { (success, error) in
fetchGroup.leave()
}
}

You need to enter() all dispatched tasks before any task in the group will leave().
Move your fetchGroup.enter() before DispatchQueue.concurrentPerform(...
let endPoints = [.email, .others ]
endPoints.forEach {_ in fetchGroup.enter()} //<- add this line
DispatchQueue.concurrentPerform(iterations: endPoints.count) { (index) in
And remove fetchGroup.enter() in each task:
fileprivate func updateEmail(_ parent: Parent,
fetchGroup: DispatchGroup,
completionBlock: #escaping (_ success: Bool, _ error: DescriptiveErrorType?) -> Void) {
//fetchGroup.enter() //<- remove this line, in your `update(...)` as well
updateEmail(parent: parent) { (success, error) in
fetchGroup.leave()
}
}
Please try.

Related

How can I chain completion handlers if one method also has a return value?

I have 2 methods I need to call, the second method must be executed using the result of the first method and the second method also returns a value.
I have put together a simple playground that demonstrates a simple version of the flow
import UIKit
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
completion("some.access.token")
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
completion(.success((data, response)))
}
})
task.resume()
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
I need to use the token returned by AuthTokenLoader as a header on the request sent by dispatch method in my Networking class.
Networking also returns a task so this request can be cancelled.
As I cannot return from inside the completion block of the AuthTokenLoader load completion, I unsure how to achieve this.
You can create a wrapper for your task and return that instead.
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
Your entire playground would become
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion("some.access.token")
}
}
}
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task {
let task = URLSessionTaskWrapper(completion)
loader.load("token") { token in
task.wrapped = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
task.complete(with: .failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
task.complete(with: .success((data, response)))
}
})
task.wrapped?.resume()
}
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
Turns out it was harder to do in Combine than I thought. Like most people, I'm still quite new to this. Would happily accept edits from people who know better :)
The general principle is that, instead of taking a completion block, your functions should return a Publisher, that you can then choose to do things with, and chain together.
So your token loader can look like this:
protocol TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error>
}
Instead of taking a completion block, you now return a publisher which will send you a string on success, or an error if something goes wrong.
And your implementation, well I wasn't sure what you were planning on doing in there but here's an example of sorts:
class AuthTokenLoader: TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error> {
print("was called")
// Do async stuff to create your token, ending in a result
let result = Result<String, Error>.success("some.access.token")
return result.publisher.eraseToAnyPublisher()
}
}
Your client can look like this:
protocol Client {
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error>
}
Now this is the complicated bit. What you want to do is take the publisher from your token loader, and when it gives a result, make your URL request, and then make another publisher from that URL request. URLSession can give you a publisher for a data task, and there is a flatMap operator which is supposed to allow you to turn the results of one publisher into a new publisher, but you get stuck in the weeds of the type system so the code is uglier than it ought to be:
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error> {
return loader.load("someKey")
.flatMap {
token -> AnyPublisher<Data, Error> in
var finalRequest = request
finalRequest.setValue(token, forHTTPHeaderField: "x-token")
return URLSession.shared.dataTaskPublisher(for: finalRequest)
.map { $0.data }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
You'd use this code like so:
let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
let sub = client.dispatch(request)
.sink(receiveCompletion: {
switch $0 {
case .finished: print("Done")
case .failure(let error): print("Error :\(error)")
}
}, receiveValue: { data in
print("Data: \(data)")
})
sub is an AnyCancellable, so if it is deallocated or you call cancel on it, this passes back up the chain and it will cancel the URL task for you.
If you want to do things with the data then there are operators for mapping or decoding or whatever which makes the whole thing very nice to work with.

How to unit test async code which has DispatchQueue.main.async in it

I've written below swift code
public func authenticateTouchID(completion: #escaping (_ result: Bool, _ error: Error?) -> Void) {
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString("temp", comment: ""),
reply: { (result, error) -> Void in
DispatchQueue.main.async {
guard let error = error else {
completion(result, nil)
return
}
completion(result, error)
}
}
)}
and while unit test it I'm facing issue.
This works perfectly on local but while generating teamcity build it get failed.
please let me know how can I unit test above piece of code?
Let me first suggest eliminating the extra code around calling your completion handler. The guard statement is redundant.
public func authenticateTouchID(completion: #escaping (_ result: Bool, _ error: Error?) -> Void) {
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString("temp", comment: ""),
reply: { (result, error) -> Void in
DispatchQueue.main.async {
completion(result, error)
}
}
)}
Next, I would suggest passing in the queue that you want to run the completion on ... but with a default value of DispatchQueue.main so that your application call sites don't change but in your test code you can pass in something else.
public func authenticateTouchID(completion: #escaping (_ result: Bool, _ error: Error?) -> Void, onQueue queue: DispatchQueue = DispatchQueue.main) {
authenticationContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: NSLocalizedString("temp", comment: ""),
reply: { (result, error) -> Void in
queue.async {
completion(result, error)
}
}
)}

Correct memory management in closures

I have method with closure.
It works like this
func ping() -> pingProtocol {
let factory = pingFactory()
let ping = factory.getPing() // returns pingProtocol conforming instance, for example "PingService"
return ping
}
func performPing(success: #escaping () -> (), fail: #escaping () -> ()) {
ping().status { (ready, response) in
if ready {
success()
} else {
fail()
}
}
}
Realisation of ping is abstraction over Alamofire.
class PingService {
func status(completion: #escaping (Bool, ServiceResponse) -> Void) {
doSimpleRequest(endPoint: "/api/get_state/", param: [:], completion: completion)
}
func doSimpleRequest(endPoint: String, param: [String: Any], completion: #escaping (Bool, ServiceResponse) -> Void) {
APIManager.shared.postRequest(mode: APIMode.Service, endPoint: endPoint, parameters: param) { [weak self] (response, error) in
guard let strongSelf = self else {
return
}
guard let response = response as? [String: Any] else { return }
let serviceResponse = ServiceResponse(dict: response)
if error != nil {
completion(false, serviceResponse)
return
}
completion(true, serviceResponse)
}
}
}
The problem is that with weak self guard in self is always triggers. If I use unowned, I get crash. If I dont use weak or unowned it works fine.
The ping method itself lives in viewmodel of controller, and controller stays on screen when completion block is needed to be executed, so the problem is in deallocation of ping instance, not in viewmodel thats holds it.
If I use capture list I dont get desired behavior, but If I do I suspect that I get memory leak.
What is best practice here ?

Why am I getting this error: Ambiguous reference to member 'fetch(with:parse:completion:)'

I am using Xcode 8.3.3. I am getting a Swift Compiler Error "Ambiguous reference to member". I have gone over the code and can't seem to figure it out.
protocol APIClient {
var session: URLSession { get }
func fetch<T: JSONDecodable>(with request: URLRequest, parse: #escaping (JSON) -> T?, completion: #escaping (Result<T, APIError>) -> Void)
func fetch<T: JSONDecodable>(with request: URLRequest, parse: #escaping (JSON) -> [T], completion: #escaping (Result<[T], APIError>) -> Void)
}
fetch(with: request, parse: { json -> [YelpBusiness] in
guard let businesses = json["businesses"] as? [[String: Any]] else { return [] }
return businesses.flatMap { YelpBusiness(json: $0) }
}, completion: completion)
https://github.com/jripke74/RestaurantReviews.git
I checked your code. You have created a protocol which confirms the class YelpClient. What's wrong?
fetch(with: request, parse: { json -> [YelpBusiness] in
guard let businesses = json["businesses"] as? [[String: Any]] else { return [] }
return businesses.flatMap { YelpBusiness(json: $0) }
}, completion: completion)
Using above code you are directly calling the protocol without any delegate. The problem is you need to inherit the protocol in the YelpClient class like below.
func fetch<T>(with request: URLRequest, parse: #escaping (APIClient.JSON) -> T?, completion: #escaping (Result<T, APIError>) -> Void) where T : JSONDecodable {
//Code
}
func fetch<T>(with request: URLRequest, parse: #escaping (APIClient.JSON) -> [T], completion: #escaping (Result<[T], APIError>) -> Void) where T : JSONDecodable {
//Code
}
For more details refer to apple documentation
Updated:
Check the below screenshot for better idea

Creating a closure inline

In WKWebView we have a method evaluateJavaScript. I have overridden this method to add custom code.
override open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Swift.Void)? = nil)
{
let b:Bool? = true
let error: Error? = NSError(domain: "com.My.Tests", code: 1000, userInfo: nil) as Error
let completion = (b, error) -> Void
evaluateJavaScript(javaScriptString, completionHandler: completion)
}
How to pass the completion? It is throwing an error.
That's not a real closure.
The signature expects something like
let completion = { (b, error) in print(b, error) }
The declared b and error variables are useless because b and error are returned from the method.
override open func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Swift.Void)? = nil)
{
let b:Bool? = true
let error: Error? = NSError(domain: "com.My.Tests", code: 1000, userInfo: nil) as Error
evaluateJavaScript(javaScriptString) { (result, error) in
if error != nil {
completionHandler?(nil, error)
} else {
completionHandler?(result, nil)
}
}
}
Your completion is not a closure. It should be something like
let completion = { (b: Any?, error: Error?) -> Void in
print("\(error)")
}
I'm presuming that you want your WKWebView to act like some Javascript evaluation failed. If that's the case, you'd either:
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
evaluateJavaScript(javaScriptString) { b, error in
completionHandler?(true, NSError(domain: "com.My.Tests", code: 1000))
}
}
Or, if you didn't really need to evaluate the Javascript, but just look like a failure (e.g. stubbing it in a test):
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
completionHandler?(true, NSError(domain: "com.My.Tests", code: 1000))
}

Resources