Search code examples
iosswiftnsurlsessioncompletion-block

Is There A Neat Way to Attach a Completion Block to an NSURLSessionDataDelegate Callback in Swift?


OK, here's the deal:

I have a URL call I'm making in a Swift app, sort of like so:

/*!
    @brief Tests a given Root Server URL for validity

    @discussion What we do here, is append "/client_interface/serverInfo.xml"
                to the given URI, and test that for validity.

    @param inURIAsAString This contains a string, with the URI.

    @param inCompletionBlock This is the completion block supplied by the caller. It is to be called upon receipt of data.

    @returns an implicitly unwrapped optional String. This is the given URI, "cleaned up."
*/
class func testRootServerURI(inURIAsAString:String, inCompletionBlock:requestCompletionBlock!) -> String! {
    // First, trim off any trailing slashes.
    var ret:String! = inURIAsAString.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: "/"))
    // Next, make the string entirely lowercase.
    ret = ret.lowercaseString

    // Add the "http://" if necessary.
    if(!ret.beginsWith ("http")) {
        ret = "http://" + ret
    }

    // This is the URI we will actually test.
    let testURIString = ret + "/client_interface/serverInfo.xml"

    #if DEBUG
        print("Testing \(testURIString).")
    #endif

    let url:NSURL! = NSURL(string: testURIString)

    // We can't have the URL already in play. That would be bad.
    if(nil == self.urlExtraData[testURIString]) {
        // Assuming we have a completion block and a URI, we will actually try to get a version from the server (determine its validity).
        if((nil != inCompletionBlock) && (nil != ret)) {
            // Store the completion block for recall later.
            self.urlExtraData.updateValue(inCompletionBlock, forKey: testURIString)
            let dataTask:NSURLSessionTask = BMLTAdminAppDelegate.connectionSession.dataTaskWithURL(url)!

            dataTask.resume()
        }
    }
    else {
        ret = nil
    }

    return ret
}

Actually, exactly like that, as it's the function that I'm using (a static class function).

The problematic line is this one:

self.urlExtraData.updateValue(inCompletionBlock, forKey: testURIString)

"urlExtraData" is a Dictionary that I declare earlier:

/*!
    This is a dictionary of callbacks for open requests. It keys on the URL called.

    @discussion I hate pulling crap like this, as it's clumsy and thread-unfriendly. Unfortunately,
                there doesn't seem to be much choice, as there's no way to attach a refCon to a URL
                task.
*/
static var urlExtraData:Dictionary<String,requestCompletionBlock!>! = nil

and assign it here:

// Set up an empty dictionary for the URL refCon data.
BMLTAdminAppDelegate.urlExtraData = Dictionary<String,requestCompletionBlock!>()

The completion block typedef is here:

/*!
    @brief This is the definition for the testRootServerURI completion block.

    @discussion The routine is called upon completion of a URL connection. When the
                connection ends (either successfully or not), this routine is called.
                If it is successful, then the inData parameter will be non-nil.
                If it failed, then the parameter will be nil.

    @param inData   the Data returned.
*/
typealias requestCompletionBlock = (inData: NSData!)->Void

The session setup is here:

BMLTAdminAppDelegate.connectionSession = NSURLSession(configuration: config, delegate:self, delegateQueue: NSOperationQueue.mainQueue())

The delegate response handler is is here:

/*!
    @brief Called when a task receives data.

    @param session The NSURLSession that controls this task.
    @param dataTask The task responsible for this callback.
    @param data The data returned.
*/
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
    let url                                 = dataTask.currentRequest?.URL
    // We don't do squat if there is no callback in our table.
    if(nil != BMLTAdminAppDelegate.urlExtraData.indexForKey((url?.absoluteString)!)) {
       // Fetch the stored comnpletion block from our dictionary.
        let callback:requestCompletionBlock!    = BMLTAdminAppDelegate.urlExtraData[(url?.absoluteString)!]

        // If one was provided (always check), then call it.
        if(nil != callback) {
            BMLTAdminAppDelegate.urlExtraData.removeValueForKey((url?.absoluteString)!) // Remove the callback from the dictionary.
            callback(inData: data)
        }
    }
}

I think using a dictionary to hold the second callback is a nasty, smelly hack. However, I have found no way to attach a refCon to the task. I'd much rather attach the secondary completion block directly to the task, as opposed to using a separate dictionary.

I'd love to be told my methodology is bad, as long as I am given something better.

Any takers?

Thanks!


Solution

  • What I've done in the past is to create some sort of transaction object. I set up a download manager (as a singleton) to create an array of transaction objects. I make the transaction object the delegate of the URL session (Actually this predates NSURLSession - I did it with NSURLConnection, but the idea is the same.)

    The transaction object also has a completion block parameter. Then when the download completes, the transaction object invokes it's completion block, and then notifies the download manager singleton that it's done and ready to be cleaned up.