Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic completion handler in Swift

I have a method which has a method named performRequest(). It takes a JSONRequest parameter. JSONRequest looks something like this:

public typealias JSONCompletionHandler = ([Entity]?, NSError?) -> Void

public class JSONRequest: Request {
    public var completionHandler: JSONCompletionHandler
    public var endPoint: String
}

And performRequest() looks like this:

public func performJSONRequest<T where T: Entity>(jsonRequest: JSONRequest, _: Type) {
        // Make a request which returns a data object
        var entities = self.convertJSONData(data, jsonKey: jsonRequest.jsonKey, T.self)
        // Error: 'T' is not identical to 'Entity'
        jsonRequest.completionHandler(entities, error)
}

As you can see, it calls convertJSONData() which looks like this:

func convertJSONData<T where T: Entity>(jsonData: AnyObject, _: T.Type) -> [T] {
        // Convert the data into Swift collection classes, enumerate over them, and create model objects
        var json = JSON(data: jsonData as NSData, options: nil, error: nil)
        var entities = [T]()

        for obj in json {
            let book = T(json: obj)
            entities.append(book)
        }

    return entities

Entity is a protocol which all my model classes, for example Author and Book, conform to.

It defines one method: init(json: JSON). Since T is defined as T:Entity, I can just call T:(json: obj) to create instances of any class conforming to Entity.

I want to be able to use performJSONRequest() to perform request for any object conforming to Entity. For example, I want to build a request for Book instances like this:

var request = JSONRequest(endPoint: "books") { (let object: [Entity]?, let error: NSError?) -> Void in
    // Cast object to [Book] and have fun
}

performJSONRequest<Book>(request)

I can't for the life of me find out how I would implement this. Right now, I get an error in the performJSONRequest() method saying 'T' is not identical to 'Entity'. If I define the array in the completion handler as [AnyObject] I get the same error: 'T' is not identical to 'AnyObject'.

Thanks for any help!

like image 211
wander Avatar asked Dec 06 '14 16:12

wander


People also ask

What is a completion handler Swift?

A completion handler in Swift is a function that calls back when a task completes. This is why it is also called a callback function. A callback function is passed as an argument into another function. When this function completes running a task, it executes the callback function.

How do I pass a completion handler in Swift?

Make a Completion handler : Create a completion handler closure and than pass it to function. Here we create an completion handler closure which type is () → Void . Now We will create a function which can take this closure .. Here we create a function which take a parameter of type () →Void .

What does the @escaping keyword do when declaring a completion handler?

The function declaration also contains a special keyword: @escaping. This means that the closure that's passed in as an argument for this parameter can escape the dataTask(with:completionHandler:) function. In other words, the closure is called after the function dataTask() finishes executing.


1 Answers

The solution is to move the generic type up into the JSONRequest class - that way JSONCompletionHandler can be defined with the generic type you're requesting instead of just the Entity protocol. (Some of your code seemed a little pseudo-, so this might need some tweaking to fit back into your implementation.)

JSONRequest is now a generic class with an Entity type restraint:

public class JSONRequest<T: Entity>: Request {
    // completion handler defined in terms of `T`
    public typealias JSONCompletionHandler = ([T]?, NSError?) -> Void

    // no further changes        
    public var completionHandler: JSONCompletionHandler
    public var endPoint: String
    public init(endPoint: String, completionHandler: JSONCompletionHandler) {
        self.endPoint = endPoint
        self.completionHandler = completionHandler
    }
}

performJSONRequest doesn't need the type passed as a separate parameter any more. Since jsonRequest is specialized, it gets the type information from that parameter:

public func performJSONRequest<T: Entity>(jsonRequest: JSONRequest<T>) {
    // create array of `T` somehow 
    var entities: [T] = []
    var error: NSError?

    // completionHandler expects [T]? and NSError?
    jsonRequest.completionHandler(entities, error)
}

When creating your JSONRequest instance, the type given in the completion handler (e.g., [Book]?) will set the type for the generic JSONRequest, and hold throughout the process:

var request = JSONRequest(endPoint: "books") { (books: [Book]?, error) in
    println(books?.count)
}
performJSONRequest(request)
like image 107
Nate Cook Avatar answered Oct 04 '22 19:10

Nate Cook