How can I synchronize closures?
I have this code:
private func getWeather(parameters: [String : Any], failure: ((String) -> Void)? = nil ,completion: (() -> Void)? = nil) {
for _ in 0...10 {
RequestManager.sharedInstance.request(url: baseURL, parameters: parameters, completion: { (result) in
if JSON.parse(result)["name"].string == nil {
failure?("Something went wrong. Please, try again later")
} else {
let weatherModel: WeatherModel = WeatherModel(json: JSON.parse(result))
}
})
}
completion?()
}
In my code, completion?() will call, not when all requests will ended
And I need call completion?() when all requests will ended. Ho can I do it?
Since the currently accepted answer isn't correct, here is a version that properly uses a DispatchGroup.
private func getWeather(parameters: [String : Any], failure: ((String) -> Void)? = nil ,completion: (() -> Void)? = nil) {
let dispatchGroup = DispatchGroup()
for _ in 0...10 {
dispatchGroup.enter()
RequestManager.sharedInstance.request(url: baseURL, parameters: parameters) { result in
if JSON.parse(result)["name"].string == nil {
failure?("Something went wrong. Please, try again later")
} else {
let weatherModel: WeatherModel = WeatherModel(json: JSON.parse(result))
}
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
completion?()
}
}
An easy way to do this is just to count completed calls.
private func getWeather(parameters: [String : Any], failure: ((String) -> Void)? = nil ,completion: (() -> Void)? = nil) {
let numCalls = 11;
var completedCalls = 0;
for _ in 0..<numCalls {
RequestManager.sharedInstance.request(url: baseURL, parameters: parameters, completion: { (result) in
if JSON.parse(result)["name"].string == nil {
failure?("Something went wrong. Please, try again later")
} else {
let weatherModel: WeatherModel = WeatherModel(json: JSON.parse(result))
}
completedCalls += 1
if completedCalls == numCalls {
completion?()
}
})
}
}
Your completion callback runs when each request ends. Making each of your completion callbacks update a value in its enclosing scope allows you to keep track of how many requests have ended. When all the requests you expect have ended, you call completion?().
Related
I have a task aggregator which aggregates and executes tasks in a synchronized manner. It's thread safe and all that. Here's what it looks like
class DaddysMagicalTaskAggregator {
private let tasks: ThreadSafeValueContainer<[KewlTask]>
private let cancelled: ThreadSafeValueContainer<Bool>
private var completion: ((Result<Bool, Error>) -> Void)?
private var alreadyCompleted: Result<Bool, Error>?
private var getTasks: [KewlTask] {
return tasks.value ?? []
}
private var isCancelled: Bool {
return cancelled.value ?? true
}
init(queue: DispatchQueue = DispatchQueue(label: "DaddysMagicalTaskAggregator")) {
self.tasks = ThreadSafeValueContainer(value: [], queue: queue)
self.cancelled = ThreadSafeValueContainer(value: false, queue: queue)
}
/// Add a task to the list of tasks
func addTask(_ task: KewlTask) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + [task]
return self
}
/// Add tasks to the list of tasks
func addTasks(_ tasks: [KewlTask]) -> DaddysMagicalTaskAggregator {
self.tasks.value = getTasks + tasks
return self
}
/// Start executing tasks
#discardableResult
func run() -> DaddysMagicalTaskAggregator {
guard !isCancelled else {
return self
}
guard !getTasks.isEmpty else {
alreadyCompleted = .success(true)
completion?(.success(true))
return self
}
var currentTasks = getTasks
let taskToExecute = currentTasks.removeFirst()
self.tasks.value = currentTasks
taskToExecute.execute { (result) in
switch result {
case .success:
self.run()
case.failure(let error):
self.taskFailed(with: error)
}
}
return self
}
private func taskFailed(with error: Error) {
tasks.value = []
alreadyCompleted = .failure(error)
completion?(.failure(error))
completion = nil
}
/// Add a completion block which executes after all the tasks have executed or upon failing a task.
func onCompletion(_ completion: #escaping (Result<Bool, Error>) -> Void) {
if let result = alreadyCompleted {
completion(result)
} else {
self.completion = completion
}
}
/// Cancel all tasks
func cancelAllTasks(with error: Error) {
cancelled.value = true
taskFailed(with: error)
}
}
public class KewlTask {
private let closure: ((KewlTask) -> Void)
private var completion: ((Result<Bool, Error>) -> Void)?
public init(_ closure: #escaping (KewlTask) -> Void) {
self.closure = closure
}
public func execute(_ completion: #escaping (Result<Bool, Error>) -> Void) {
self.completion = completion
closure(self)
}
/// Task succeeded
func succeeded() {
completion?(.success(true))
}
/// Task failed with given error
func failed(with error: Error) {
completion?(.failure(error))
}
/// Take action based on the result received
func processResult(_ result: Result<Bool, Error>) {
switch result {
case .success:
succeeded()
case .failure(let error):
failed(with: error)
}
}
}
public class ThreadSafeContainer {
fileprivate let queue: DispatchQueue
public init(queue: DispatchQueue) {
self.queue = queue
}
}
public class ThreadSafeValueContainer<T>: ThreadSafeContainer {
private var _value: T
public init(value: T, queue: DispatchQueue) {
self._value = value
super.init(queue: queue)
}
public var value: T? {
get {
return queue.sync { [weak self] in
self?._value
}
}
set(newValue) {
queue.sync { [weak self] in
guard let newValue = newValue else { return }
self?._value = newValue
}
}
}
}
It works as expected. However, when I write a test to make sure it deallocates, the test keeps failing even after the expectation is fulfilled.
Please look at the test code below
import XCTest
class AggregatorTests: XCTestCase {
func testTaskAggregatorShouldDeallocatateUponSuccess() {
class TaskContainer {
let aggregator: CustomKewlAggregator
init(aggregator: CustomKewlAggregator = CustomKewlAggregator()) {
self.aggregator = aggregator
}
}
class CustomKewlAggregator: DaddysMagicalTaskAggregator {
var willDeinit: (() -> Void)?
deinit {
willDeinit?()
}
}
let myExpecation = self.expectation(description: "Task Aggregator should deallocate")
var container: TaskContainer? = TaskContainer()
let t1 = KewlTask { t in
t.succeeded()
}
let t2 = KewlTask {
$0.succeeded()
}
container?.aggregator.willDeinit = {
myExpecation.fulfill()
}
container?.aggregator
.addTasks([t1, t2])
.run()
.onCompletion { (result) in
container = nil
}
waitForExpectations(timeout: 4, handler: nil)
}
}
I've added breakpoints and everything to ensure the expectation fulfillment code does execute.
It doesn't seem to be an xCode issue since I've tested it on XCode 11.7, 12.1, 12.2, and 12.4.
Any idea what's going on here? To me, it looks like a bug in XCTests.
In UIWebview implementation I had something like:-
if let pageBody = webView?.stringByEvaluatingJavaScript(from: "document.body.innerHTML") {
if pageBody.contains("xyz") {
return webView?.stringByEvaluatingJavaScript(from:
"document.getElementById('xyz').innerHTML")
}
}
I am trying to migrate this to WKWebview:-
I did something like this but the return value gets lost in the nested completion handlers:-
wkWebView?.evaluateJavaScript("document.body.innerHTML", completionHandler: { (pageBody, nil) in
if let pBody = (pageBody as? String)?.contains("xyz"), pBody {
wkWebView?.evaluateJavaScript("document.getElementById('xyz').innerHTML", completionHandler: { (result, error) in
resultString = result as? String
})
}
})
return resultString
evaluateJavaScript is run asynchronously (unlike stringByEvaluatingJavaScript which will wait until the javascript has been evaluated and return the result), so resultString hasn't been set by the time you return it. You will need to organize your code so that the result of javascript is used after the completion handler has been run. Something like this:
func getElementXYZ(_ completionHandler: #escaping (String?) -> Void) {
wkWebView?.evaluateJavaScript("document.body.innerHTML") { (pageBody, nil) in
if let pBody = (pageBody as? String)?.contains("xyz"), pBody {
wkWebView?.evaluateJavaScript("document.getElementById('xyz').innerHTML") { (result, error) in
completionHandler(result as? String)
}
}
}
}
And to call the function:
self.getElementXYZ { result in
//Do something with the result here
}
Please, I need help with are problem, I changing the syntax Swift 3 for swift 4 and now i have many problems for identification of all my bugs.The Error its on the function savePhoto in the last line., completionHandler: { _ in
func takePhoto(_ previewLayer: AVCaptureVideoPreviewLayer, location: CLLocation?, completion: (() -> Void)? = nil) {
guard let connection = stillImageOutput?.connection(with: AVMediaType.video) else { return }
connection.videoOrientation = Helper.videoOrientation()
queue.async {
self.stillImageOutput?.captureStillImageAsynchronously(from: connection) {
buffer, error in
guard let buffer = buffer, error == nil && CMSampleBufferIsValid(buffer),
let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer),
let image = UIImage(data: imageData)
else {
DispatchQueue.main.async {
completion?()
}
return
}
self.savePhoto(image, location: location, completion: completion)
}
}
}
func savePhoto(_ image: UIImage, location: CLLocation?, completion: (() -> Void)? = nil) {
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
request.creationDate = Date()
request.location = location
}, completionHandler: { _ in
DispatchQueue.main.async {
completion?()
}
})
}
Rewrite it to:
}, completionHandler: { (_, _) in
As per documentation, completion handler in performChanges(_:completionHandler:) accepts two parameters, not just one. _ in, what you have used, is a placeholder for a single parameter only.
As you can clearly see in the error message the completionHandler passes two parameters rather than just one.
So you have to write
}, completionHandler: { (_, _) in
but you are strongly encouraged to handle the result and a potential error. Ignoring errors causes bad user experience.
Hello and thanks in advance for your time.
In my code I am making various requests to AWSSQS which all return AWSTask. I have found working with these AWSTask objects to be very difficult while also trying to keep all the logic specific to AWS in a single class so I can easily switch to a different cloud service if need be.
Ideally, what I would like to do is execute a series of AWS tasks asynchronously in a serial fashion. Normally I would just add tasks to a custom Serial Dispatch Queue but since The AWSTask objects are themselves asynchronous tasks, I can't do that.
Here is a simple example that illustrates the problem I am having. It doesn't have any real world purpose but it does a good job illustrating the problem. Below, I have code to create a SQS queue, send a message to a SQS queue, receive a message from an SQS queue, and delete a SQS queue. Let's say I want to do those four things in a serial, asynchronous fashion. In other words, I want to make sure the previous task succeeded before attempting the next task.
ViewController
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
awsClass.runTest()
DispatchQueue.main.async {
print("Test Finished")
}
}
AwsClass
public func createQueue(){
guard let createQueueRequest = AWSSQSCreateQueueRequest() else{fatalError()}
createQueueRequest.queueName = "TestQueue"
sqs.createQueue(createQueueRequest).continueWith(block: {(task) -> AnyObject? in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
self.queueUrl = task.result!.queueUrl!
print("created queue at: \(self.queueUrl!)")
}
return nil
})
}
public func deleteQueue(){
if queueUrl != nil {
guard let deleteQueueRequest = AWSSQSDeleteQueueRequest() else{fatalError()}
deleteQueueRequest.queueUrl = queueUrl
sqs.deleteQueue(deleteQueueRequest).continueWith(block: {(task) -> AnyObject? in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
print("queue sucessfully deleted from \(self.queueUrl!)")
self.queueUrl = nil
}
return nil
})
}
else{
print("Queue has already been deleted")
}
}
public func sendMessage(messageData: String, toConnectId: String) {
guard let sendMessageRequest = AWSSQSSendMessageRequest() else{fatalError()}
sendMessageRequest.queueUrl = toConnectId
sendMessageRequest.delaySeconds = 0
sendMessageRequest.messageBody = messageData
sqs.sendMessage(sendMessageRequest).continueWith(block: {(task) -> AnyObject? in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
print("successfully sent message to \(toConnectId)")
}
return nil
})
}
public func receiveMessage(){
guard let receiveMessageRequest = AWSSQSReceiveMessageRequest() else{fatalError()}
receiveMessageRequest.queueUrl = self.queueUrl
receiveMessageRequest.maxNumberOfMessages = 1
sqs.receiveMessage(receiveMessageRequest).continueWith(block: {(task) -> AnyObject? in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
let message = (task.result?.messages?.first)!
print("successfully received message with body: \(message.body ?? "failed")")
}
return nil
})
}
public func runTest(){
let mySerialQueue = DispatchQueue(label: "mySerialQueue")
mySerialQueue.sync {
self.createQueue()
}
mySerialQueue.sync {
self.sendMessage(messageData: "test", toConnectId: "https://someUrl")
}
mySerialQueue.sync {
self.receiveMessage()
}
mySerialQueue.sync {
self.deleteQueue()
}
}
Since the AWSTasks are asynchronous with completion functions, the code quickly makes all four calls and then the completion functions are called whenever those tasks finish. Instead I want the completion function of the first task to finish before the next task begins.
The AWSTask objects are meant to be "chained" together.
Documentation can be found here: http://docs.aws.amazon.com/mobile/sdkforios/developerguide/awstask.html
A small example here:
sqs.createQueue(/* parameters */).continueWithSuccess(block: {(task) -> Void in
// Success
return sqs.sendMessage(/* parameters */)
}).continueWithSuccess(block: {(task) -> Void in
// Success
return sqs.receiveMessage(/* parameters */)
}).continueWithSuccess(block: {(task) -> Void in
// Success
return sqs.deleteQueue(/* parameters */)
})
Alright, so I found a solution to my question. It works exactly as desired but it does so in this nasty chain of completion functions. If anyone knows a more elegant solution, I am all ears!
ViewController
print("Starting Test")
DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async {
atomConnector.runTest(completion: {
print("test finshed")
})
}
AwsClass
public func createQueue(completion: #escaping () -> Void){
guard let createQueueRequest = AWSSQSCreateQueueRequest() else{fatalError()}
createQueueRequest.queueName = "TestQueue"
sqs.createQueue(createQueueRequest).continueWith(block: {(task) -> Void in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
self.queueUrl = task.result!.queueUrl!
print("created queue at: \(self.queueUrl!)")
completion()
}
})
}
public func deleteQueue(completion: #escaping () -> Void){
if queueUrl != nil {
guard let deleteQueueRequest = AWSSQSDeleteQueueRequest() else{fatalError()}
deleteQueueRequest.queueUrl = queueUrl
sqs.deleteQueue(deleteQueueRequest).continueWith(block: {(task) -> Void in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
print("queue sucessfully deleted from \(self.queueUrl!)")
self.queueUrl = nil
completion()
}
})
}
else{
print("Queue has already been deleted")
}
}
public func sendMessage(messageData: String, toConnectId: String, completion: #escaping () -> Void) {
guard let sendMessageRequest = AWSSQSSendMessageRequest() else{fatalError()}
sendMessageRequest.queueUrl = toConnectId
sendMessageRequest.delaySeconds = 0
sendMessageRequest.messageBody = messageData
sqs.sendMessage(sendMessageRequest).continueWith(block: {(task) -> Void in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
print("successfully sent message to \(toConnectId)")
completion()
}
})
}
public func receiveMessage(completion: #escaping () -> Void){
guard let receiveMessageRequest = AWSSQSReceiveMessageRequest() else{fatalError()}
receiveMessageRequest.queueUrl = self.queueUrl
receiveMessageRequest.maxNumberOfMessages = 1
sqs.receiveMessage(receiveMessageRequest).continueWith(block: {(task) -> Void in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
let message = (task.result?.messages?.first)!
print("successfully received message with body: \(message.body ?? "failed")")
self.deleteMessage(receiptHandle: message.receiptHandle, completion: completion)
}
})
}
public func deleteMessage(receiptHandle: String?, completion: #escaping () -> Void){
guard let deleteMessageRequest = AWSSQSDeleteMessageRequest() else{fatalError()}
deleteMessageRequest.queueUrl = self.queueUrl
deleteMessageRequest.receiptHandle = receiptHandle
sqs.deleteMessage(deleteMessageRequest).continueWith(block: {(task) -> Void in
if task.error != nil {
print(task.error!)
}
else if task.result != nil {
print("successfully deleted message with receiptHandle: \(receiptHandle)")
completion()
}
})
}
public func runTest(completion: #escaping () -> Void){
self.createQueue(completion: {
self.sendMessage(messageData: "test", toConnectId: "https://someUrl", completion: {
self.receiveMessage(completion: {
self.deleteQueue(completion: {
completion()
})
})
})
})
}
lets propose this scenario
a method with async network operations
func asyncMethodA() -> String?
{
result : String?
Alamofire.manager.request(.POST, "https://www.apiweb.com/apimethod", parameters: parameters, encoding:.JSON)
.response { (request, response, rawdata, error) in
if (response?.statusCode == 200)
{
//DO SOME HEAVY LIFTING
}
}
return result //string
}
another method with async network operations
func asyncMethodB() -> String?
{
result : String?
Alamofire.manager.request(.POST, "https://www.yetanotherapiweb.com/apimethod", parameters: parameters, encoding:.JSON)
.response { (request, response, rawdata, error) in
if (response?.statusCode == 200)
{
//DO SOME HEAVY LIFTING
}
}
return result //string
}
a method in which i shall call those methods A and B, to do some operations
func displayResult
{
1) let a = asyncMethodA()
2) let b = asyncMethodB()
3) println(a + b) //some chaotic stuff might happen :(
}
so the question is how i could make that (2) waits for (1) to run, and (3) waits for (2) and so on (that 1,2 and 3 run syncronised)?
(i know that one answer is to chain asyncMethodA and displayResult into asyncMethodB, but want to know if there is some other method)
thank you!.
func anAsyncMethod(resultHandler: (result: AnyObject) -> Void) {
...
}
func anotherAsyncMethod(resultHandler: (result: AnyObject) -> Void) {
...
}
let operationQueue = NSOperationQueue()
func performWithCompletionHandler(completion: (AnyObject?, AnyObject?) -> Void) {
var resultOfOperation1: AnyObject?
var resultOfOperation2: AnyObject?
let operation1 = NSBlockOperation {
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
self.anAsyncMethod {
result in
resultOfOperation1 = result
dispatch_group_leave(dispatchGroup)
}
// wait until anAsyncMethod is completed
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)
}
let operation2 = NSBlockOperation {
let dispatchGroup = dispatch_group_create()
dispatch_group_enter(dispatchGroup)
self.anotherAsyncMethod {
result in
resultOfOperation2 = result
dispatch_group_leave(dispatchGroup)
}
// wait until anotherAsyncMethod is completed
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER)
}
let completionOperation = NSBlockOperation {
// send all results to completion callback
completion(resultOfOperation1, resultOfOperation2)
}
// configuring interoperation dependencies
operation2.addDependency(operation1)
completionOperation.addDependency(operation2)
operationQueue.addOperations([operation1, operation2, completionOperation], waitUntilFinished: false)
}
Thanks Yimin for the code above. I've updated it to the latest Swift syntax so just posting to be helpful:
func anAsyncMethod(resultHandler: (_ result: AnyObject) -> Void) {
...
}
func anotherAsyncMethod(resultHandler: (_ result: AnyObject) -> Void) {
...
}
func performWithCompletionHandler(completion: #escaping (AnyObject?, AnyObject?) -> Void) {
let operationQueue = OperationQueue()
var resultOfOperation1: AnyObject?
var resultOfOperation2: AnyObject?
let operation1 = BlockOperation {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
self.anAsyncMethod {
result in
resultOfOperation1 = result
dispatchGroup.leave()
}
// wait until anAsyncMethod is completed
dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
let operation2 = BlockOperation {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
self.anotherAsyncMethod {
result in
resultOfOperation2 = result
dispatchGroup.leave()
}
// wait until anotherAsyncMethod is completed
dispatchGroup.wait(timeout: DispatchTime.distantFuture)
}
let completionOperation = BlockOperation {
// send all results to completion callback
completion(resultOfOperation1, resultOfOperation2)
}
// configuring interoperation dependencies
operation2.addDependency(operation1)
completionOperation.addDependency(operation2)
operationQueue.addOperations([operation1, operation2, completionOperation], waitUntilFinished: false)
}
With the below, you can launch both async methods at the same time and do your heavy lifting after whichever one finishes last.
var methodAFinished = false
var methodBFinished = false
func asyncMethodA() -> String?
{
Alamofire.manager.request(.POST, "https://www.apiweb.com/apimethod", parameters: parameters, encoding:.JSON)
.response { (request, response, rawdata, error) in
if (response?.statusCode == 200) {
methodAFinished = true
doStuff()
}
}
return result //string
}
The guts of asyncMethodB would be methodBFinished = true; doStuff()
func doStuff() {
if methodAFinished && methodBFinished {
// do crazy stuff
}
}