22

How do I make asynchronous callbacks in swift? I'm writing a little Framework for my app because it's supposed to run on both, iOS und OS X. So I put the main code that is not device-specific into this framework that also handles requests to my online api. And obviously I also want the app's GUI and therefore my ViewControllers to react as soon as a api request has finished. In Objective-C I've done this by saving the view containing the function that had to be called in an id variable and the function itself in a selector variable. Then I invoked the function using the following code:

SEL selector = callbackMethod;
((void (*)(id, SEL))[callbackViewController methodForSelector:selector])(callbackViewController, selector);

How can I accomplish this in swift? Or is there a better way of doing this?

I really appreciate all your help!

Shruti Thombre
  • 989
  • 4
  • 11
  • 27
stoeffn
  • 556
  • 3
  • 9
  • 18
  • 1
    Just to lets you know, you can declare a Selector as string. Example: var mySelector : SEL = "somefunction:" – Duyen-Hoa Jul 31 '14 at 11:51
  • @tyt_g207 Thanks! This worked for me! Please post this as an answer so I can accept it :) – stoeffn Jul 31 '14 at 12:03
  • Your Objective-C code can be simplified to `((void (*)(id, SEL))objc_msgSend)(callbackViewController, selector);` or just `[callbackViewController performSelector:selector];` – newacct May 28 '15 at 18:56
  • In continuation with @newacct comment, in Swift the below code can be used `viewController.performSelector(onMainThread: selector, with: nil, waitUntilDone: false)` – Noor Jun 02 '20 at 09:03

4 Answers4

30

I've shared the pattern that I use for this scenario in the following gist: https://gist.github.com/szehnder/84b0bd6f45a7f3f99306

Basically, I create a singleton DataProvider.swift that setups an AFNetworking client. Then the View Controllers call methods on that DataProvider, each of which is terminated by a closure that I've defined as a typealias called ServiceResponse. This closure returns either a dictionary or an error.

It allows you to very cleanly (imo) call for an async data action from the VC's with a very clear indication of what you want performed when that async response returns.

DataProvider.swift

typealias ServiceResponse = (NSDictionary?, NSError?) -> Void

class DataProvider: NSObject {

    var client:AFHTTPRequestOperationManager?
    let LOGIN_URL = "/api/v1/login"

    class var sharedInstance:DataProvider {
        struct Singleton {
            static let instance = DataProvider()
        }
        return Singleton.instance
    }

    func setupClientWithBaseURLString(urlString:String) {
        client = AFHTTPRequestOperationManager(baseURL: NSURL.URLWithString(urlString))
        client!.operationQueue = NSOperationQueue.mainQueue()
        client!.responseSerializer = AFJSONResponseSerializer()
        client!.requestSerializer = AFJSONRequestSerializer()
    }

    func loginWithEmailPassword(email:String, password:String, onCompletion: ServiceResponse) -> Void {
        self.client!.POST(LOGIN_URL, parameters: ["email":email, "password":password] , success: {(operation:AFHTTPRequestOperation!, responseObject:AnyObject!) -> Void in

            self.setupClientWithBaseURLString("http://somebaseurl.com")

            let responseDict = responseObject as NSDictionary
                // Note: This is where you would serialize the nsdictionary in the responseObject into one of your own model classes (or core data classes)
                onCompletion(responseDict, nil)
            }, failure: {(operation: AFHTTPRequestOperation!, error:NSError!) -> Void  in
                onCompletion(nil, error)
            })
    }
}

MyViewController.swift

import UIKit

class MyViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }

    override func viewWillAppear(animated: Bool)  {
        super.viewWillAppear(animated)
        DataProvider.sharedInstance.loginWithEmailPassword(email:"some@email.com", password:"somepassword") { (responseObject:NSDictionary?, error:NSError?) in

            if (error) {
                println("Error logging you in!")
            } else {
                println("Do something in the view controller in response to successful login!")
            }
        }
    }  
}
user871177
  • 721
  • 5
  • 12
  • I've now implemented your method and it works. But now there is a very strange behavior when trying to make changes to the UI in the callback: For example alerts cannot be shown and the console says `Modifications to the layout engine must not be performed from a background thread` as I try to display one. Additionally graphical changes to the view itself are delayed by a couple of seconds although the code related to these changes is executed immediately. How can I get rid of these problems? Do you have any idea? – stoeffn Jul 31 '14 at 16:51
  • 1
    I solved this problem using dispatch_async(dispatch_get_main_queue(), { ... }) – stoeffn Jul 31 '14 at 19:16
  • Is it accurate to say 'This closure returns either a dictionary or an error' if the actual closure declaration 'ServiceResponse' has a return type Void? Shouldn't you say the closure has two parameters of Type NSDictionary and of Type NSError? – riik Aug 11 '15 at 15:08
  • 1
    If we're going callbacks the javascript anyway, why not also then take it's conventions as well. `callback(err, data)` instead of `callback(data, err)` – bicycle Oct 08 '15 at 12:20
  • Would definitely be better to set operationQueue property to some other than mainQueue queue. And perform completion block on the main thread specifically. Parsing will be performed on the bg queue which is always better for fps – art-divin May 26 '19 at 18:25
24

I'd like to recommend use a block or closure callback instead of using NSThread and selectors.

For example, in my API I have follow method:

Swift:

Below you will find an updated implementation.

func getUsers(completion: (result: NSArray?, error: NSError?)->())
{
    var session = NSURLSession.sharedSession()
    var task = session.dataTaskWithRequest(request){
     (data, response, error) -> Void in
       if error != nil {
         completion(nil, error)
       } else {
         var result:NSArray = data to NSArray;
         completion(result, nil)
       }
    }
    task.resume()
}

Objective-C:

...
typedef void (^CBSuccessBlock)(id result);
typedef void (^CBFailureBlock)(NSError *error);
...

- (void)usersWithSucces:(CBSuccessBlock)success failure:(CBFailureBlock)failure
{
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:[NSURL URLWithString:url]
            completionHandler:^(NSData *data,
                                NSURLResponse *response,
                                NSError *error) {

                NSArray *users = //convert data to array

                if(error)
                    failure(error);
                else
                    success(users);
            }] resume];
}

Then, just make a call to api from view controller:

Objc:
[api usersWithSucces:^(id result)
{
   //Success callback
} failure:^(NSError *error)
{
   //Failure callback
}];

Swift:
api.getUsers({(result: AnyObject?, error: NSError?) -> Int in
    // callback here
})

UPDATE:

Meanwhile, I see that the question and answers are still being useful and interested. Well, here is an updated version of swift implementation using generic enum as a result object:

//Generic enum that represents the result
enum AsyncResult<T>
{
    case Success(T)
    case Failure(NSError?)
}


class CustomUserObject
{

}

func getUsers(completion: (AsyncResult<[CustomUserObject]>)->())
{
    let request = NSURLRequest()
    let session = NSURLSession.sharedSession()
    let task = session.dataTaskWithRequest(request){
        (data, response, error) -> Void in
        if let error = error
        {
            completion(AsyncResult.Failure(error))
        } else {
            let result: [CustomUserObject] = []//deserialization json data into array of [CustomUserObject]
            completion(AsyncResult.Success(result))
        }
    }
    task.resume()
}

//Usage:

getUsers { (result) in
    switch result
    {
    case .Success(let users):
        /* work with users*/
        break
    case .Failure(let error):
        /* present an error */
        break
    }
}
tikhop
  • 2,012
  • 14
  • 32
4

I've just made this little example: Swift: Async callback block pattern example

Basically there is ClassA:

//ClassA it's the owner of the callback, he will trigger the callback when it's the time
class ClassA {
    //The property of that will be associated to the ClassB callback
    var callbackBlock : ((error : NSError?, message : String?, adress : String? ) -> Void)?

    init() {
        //Do Your staff
    }

    //Define your function with the clousure as a parameter
    func yourFunctionWithCallback(#functionCallbackParameter : (error : NSError?,message : String?, adress : String?) -> ()) {
        //Set the calback with the calback in the function parameter
        self.callbackBlock = functionCallbackParameter
    }

    //Later On..
    func callbackTrigger() {
        self.callbackBlock?(error: nil,message: "Hello callback", adress: "I don't know")

    }
}

And ClassB:

//ClassB it's the callback reciver the callback
class ClassB {
    @IBAction func testCallbackFunction(sender: UIButton) {
        let classA = ClassA()
        classA.yourFunctionWithCallback { (error, message, adress) -> () in
            //Do your stuff
        }
    }
}

ClassA: it's the owns a property witch is the callbackBlock. ClassB will set this property by Call the yourFunctionWithCallback function. Later on then ClassA it's ready, will trigger the callback by calling the callBackBlock inside the callbackTrigger function.

ClassB: will call the ClassA method to set the callback block and wait until the block has been triggered.

Max_Power89
  • 1,710
  • 1
  • 21
  • 38
0

Can NSThread help you? :

NSThread.detachNewThreadSelector(<#selector: Selector#>, toTarget: <#AnyObject?#>, withObject: <#AnyObject?#>) 
Duyen-Hoa
  • 15,384
  • 5
  • 35
  • 44