Saturday, June 11, 2016

Completion handlers for NSURLSessionDataTask. Returning downloaded data on iOS

This tutorial picks up exactly where I left my previous iOS tutorial of "Downloading and parsing JSON data in Swift 2.2", so you may want to at least read over that one to see where we're starting from, and how we got here.

Refactor code

The very first step is refactoring our code, primarily to make more sense of how a real application would use completion handlers to pass downloaded data around.

We'll begin by creating a new Swift class called "Utilities.swift" which will inherit from NSObject:
import Foundation
class Utilities : NSObject
{
    
}
New file added, and the project structure

We will then create a new static function (didn't we use to call them method in obj-c?) called getJsonData, which will take 1 parameter, of type String, and returns Void.
The Utilities class should look like this now:
import Foundation
class Utilities : NSObject
{
    static func getJsonData(jsonUrlAsString:String) -> Void
    {
        
    }
}
We then go back to our ViewController class we were using on the first tutorial, and cut everything from the network configuration, to the resume() call. We will paste this inside of our getJsonData inside of our Utilities class.

The Utilities class should be complaining about the UILabels we set in the previous tutorial.
We will need the code for this UILabels later, but since its easy to create we will just delete it from Utilities.

At this point, our Utilities class should look like this:
import Foundation
class Utilities : NSObject
{
    static func getJsonData(jsonUrlAsString:String) -> Void
    {
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
        configuration.HTTPAdditionalHeaders = headers
        let session = NSURLSession(configuration: configuration)
        
        let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
            if (error == nil)
            {
                print("dataReceived = \(dataReceived)")
                do
                {
                    let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
                    print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
                    
                    let nameRead = dataDownloadedAsJson["name"] as? String
                    let countryRead = dataDownloadedAsJson["country"] as? String
                    let genderRead = dataDownloadedAsJson["gender"] as? String
                    let ageRead = dataDownloadedAsJson["age"] as? Int
                    
                    print("nameRead = \(nameRead!)")
                    print("countryRead = \(countryRead!)")
                    print("genderRead = \(genderRead!)")
                    print("ageRead = \(ageRead!)")
                }
                catch
                {
                    
                }
            }
            else
            {
                print("Error downloading data. Error = \(error)")
            }
        }
        
        // actually execute the task
        downloadTask.resume()
    }
}

On our ViewController we want to call the function getJsonData from the Utilities class, so we do this passing the url as a String.
At this point, the ViewController class should look like this:
import UIKit
class ViewController: UIViewController
{

    @IBOutlet weak var labelName: UILabel!
    @IBOutlet weak var labelAge: UILabel!
    @IBOutlet weak var labelGender: UILabel!
    @IBOutlet weak var labelCountry: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
        Utilities.getJsonData(jsonUrlAsString)        
    }
}
So now, if we were to run our application, we should not have any compiling errors, and we should see the output on the console displaying something like this:
dataReceived = Optional()
dataDownloadedAsJson = {
    age = 32;
    country = USA;
    gender = male;
    name = "Eduardo Flores";
}
nameRead = Eduardo Flores
countryRead = USA
genderRead = male
ageRead = 32
sdad

If the code transplant so far does the same thing as it does to me, then we're ready to move on.
Everything should work, except...well the app UI.
Before we fix that we need to create an object to return.

Create return Object
I could pass a 4 variables back, but that'll be horrible and not what you'll do on a regular basis. (you don't do that, right?)
So let's create a new swift class and let's call it "Person.swift"
In here, we'll create 4 variables: name, age, gender and country.
The whole object should look like this:
import Foundation
class Person: NSObject
{
    var name : String?
    var age : Int?
    var gender : String?
    var country : String?
}
Now that we have the object data, let's add the Person object to the getJsonData function from the Utilities class. Now the code inside getJsonData looks like this:
do
static func getJsonData(jsonUrlAsString:String) -> Void
{
    let person = Person()

    let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
    let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
    configuration.HTTPAdditionalHeaders = headers
    let session = NSURLSession(configuration: configuration)
    
    let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
        if (error == nil)
        {
            print("dataReceived = \(dataReceived)")
        do
        {
            let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
            print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
            
            let nameRead = dataDownloadedAsJson["name"] as? String
            let countryRead = dataDownloadedAsJson["country"] as? String
            let genderRead = dataDownloadedAsJson["gender"] as? String
            let ageRead = dataDownloadedAsJson["age"] as? Int
            
            print("nameRead = \(nameRead!)")
            print("countryRead = \(countryRead!)")
            print("genderRead = \(genderRead!)")
            print("ageRead = \(ageRead!)")
            
            person.name = nameRead!
            person.age = ageRead!
            person.gender = genderRead!
            person.country = countryRead!
        }
        catch
        {
            
        }
        }
        else
        {
            print("Error downloading data. Error = \(error)")
        }
    }
    
    // actually execute the task
    downloadTask.resume()
}
And now we need to return the Person object.
This is not as simple as you expect...

Return data as return type

The issue we have is that we have data in one class, but we want to use it on another class. This is a fairly common affair in programming, and the initial approach would be to return Person instead of Void from the getJsonData function.

So let's try it!

In Utilities, change getJsonData the function to this:
static func getJsonData(jsonUrlAsString:String) -> Person
{
    let person = Person()

    let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
    let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
    configuration.HTTPAdditionalHeaders = headers
    let session = NSURLSession(configuration: configuration)
    
    let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
        if (error == nil)
        {
            print("dataReceived = \(dataReceived)")
        do
        {
            let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
            print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
            
            let nameRead = dataDownloadedAsJson["name"] as? String
            let countryRead = dataDownloadedAsJson["country"] as? String
            let genderRead = dataDownloadedAsJson["gender"] as? String
            let ageRead = dataDownloadedAsJson["age"] as? Int
            
            print("nameRead = \(nameRead!)")
            print("countryRead = \(countryRead!)")
            print("genderRead = \(genderRead!)")
            print("ageRead = \(ageRead!)")
            
            person.name = nameRead!
            person.age = ageRead!
            person.gender = genderRead!
            person.country = countryRead!
        }
        catch
        {
            
        }
        }
        else
        {
            print("Error downloading data. Error = \(error)")
        }
    }
    
    // actually execute the task
    downloadTask.resume()
    return person
}
and in the ViewController, change the way you call the getJsonData function, to this (and applying the result to the UI):
override func viewDidLoad() {
    super.viewDidLoad()
    
    let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
    let person = Utilities.getJsonData(jsonUrlAsString)
    
    print("person.name = \(person.name!)")
    labelName.text = person.name!
}
If you run this, you'll be getting a runtime error of something like this:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb) 

Why are we getting this?
Well, the issue is that the getJsonData is downloading data on a different thread, and while this is great, it also means that the return statement of the getJsonData function is getting called before the data finishes downloading.
So, because of this, you're returning an empty Person object, with just nil values. Therefore person.name is nil

In other words, we're returning nothing because we're reaching our return statement before we ever even make our network call.
And that's not good...

Returning data with completion handler

Since we cannot return the data as a good ol' return type, we need a different way to return the data, after we know the download has completed....whenever that is.

For this we use Completion Handlers, also known as Callbacks in other languages.

I won't get into details on this, so if you want to learn more about Completion Handlers there are tons of online resources for that.
In short, they are a parameter sent from whoever wants to be notified of something happening on a different class or function.
In this tutorial, I'm just teaching you how to use them with a specific example to achieve something.

1. The first thing we'll do is modify the getJsonData function to accept a completion handler parameter.
Change the getJsonData function declaration from this:
static func getJsonData(jsonUrlAsString:String) -> Person
to this:
static func getJsonData(jsonUrlAsString:String, completionHandlerPerson:(responsePerson:Person?, errorPerson:NSError?) -> ())

In here, we're passing a new parameter called completionHandlerPerson, which will return 2 elements: a Person object response (named responsePerson), and a NSError object (named errorPerson)
We're also just returning () or Void now, since the return will be handled by the completionHandlerPerson.
Since our completionHandlerPerson has a Person object and a NSError object, we must always use it retuning both elements. This is handy because if we do get data downloaded, the NSError element will be nil, which is easy to check.

2. With the function declaration modified, we now need to return our data with our completionHandlerPerson element.
Our entire getJsonData function will now look like this:
static func getJsonData(jsonUrlAsString:String, completionHandlerPerson:(responsePerson:Person?, errorPerson:NSError?) -> ())
{
    let person = Person()

    let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
    let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
    configuration.HTTPAdditionalHeaders = headers
    let session = NSURLSession(configuration: configuration)
    
    let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
        if (error == nil)
        {
            print("dataReceived = \(dataReceived)")
            do
            {
                let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
                print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
                
                let nameRead = dataDownloadedAsJson["name"] as? String
                let countryRead = dataDownloadedAsJson["country"] as? String
                let genderRead = dataDownloadedAsJson["gender"] as? String
                let ageRead = dataDownloadedAsJson["age"] as? Int
                
                print("nameRead = \(nameRead!)")
                print("countryRead = \(countryRead!)")
                print("genderRead = \(genderRead!)")
                print("ageRead = \(ageRead!)")
                
                person.name = nameRead!
                person.age = ageRead!
                person.gender = genderRead!
                person.country = countryRead!
                
                completionHandlerPerson(responsePerson: person, errorPerson: nil)
            }
            catch
            {
                
            }
        }
            else
            {
                print("Error downloading data. Error = \(error)")
                completionHandlerPerson(responsePerson: nil, errorPerson: error)

            }
        }
    
    // actually execute the task
    downloadTask.resume()
}
Notice how we call the completionHandlerPerson twice: once for when the response is valid, and once for when the error happens.
We should also return a third version for when the parser fails, but that'll be your homework.

And with that, the Utilities.getJsonData function is complete.
We need to modify the caller class of ViewController.

In ViewController, you need to change this:
let person = Utilities.getJsonData(jsonUrlAsString)
for this:
Utilities.getJsonData(jsonUrlAsString,
                              completionHandlerPerson: {(responsePerson, errorPerson) -> Void in
                          })
so now we're passing our custom completion handler as a parameter to Utilities, and whenever the download thread finishes, we will be notified to do whatever want to do.

With that, all we're missing is placing the UI elements inside the caller completion handler, and we'll be done.

Here's the final ViewController class:
import UIKit
class ViewController: UIViewController
{

    @IBOutlet weak var labelName: UILabel!
    @IBOutlet weak var labelAge: UILabel!
    @IBOutlet weak var labelGender: UILabel!
    @IBOutlet weak var labelCountry: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let jsonUrlAsString = "https://api.myjson.com/bins/2c0aw"
        Utilities.getJsonData(jsonUrlAsString,
                              completionHandlerPerson: {(responsePerson, errorPerson) -> Void in
                                if (errorPerson == nil)
                                {
                                    // don't forget to update the UI in the main thread!
                                    dispatch_async(dispatch_get_main_queue()) { () -> Void in
                                        self.labelName.text = responsePerson!.name!
                                        self.labelAge.text = String(responsePerson!.age!)
                                        self.labelGender.text = responsePerson!.gender!
                                        self.labelCountry.text = responsePerson!.country!
                                    }
                                }
                                
                            })
    }
}

and here's the final Utilities class:
import Foundation
class Utilities : NSObject
{
    static func getJsonData(jsonUrlAsString:String, completionHandlerPerson:(responsePerson:Person?, errorPerson:NSError?) -> ())
    {
        let person = Person()

        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let headers: [NSObject : AnyObject] = ["Accept":"application/json", "Auth":"token"]
        configuration.HTTPAdditionalHeaders = headers
        let session = NSURLSession(configuration: configuration)
        
        let downloadTask = session.dataTaskWithURL(NSURL(string: jsonUrlAsString)!) { (dataReceived, response, error) in
            if (error == nil)
            {
                print("dataReceived = \(dataReceived)")
                do
                {
                    let dataDownloadedAsJson = try NSJSONSerialization.JSONObjectWithData(dataReceived!, options: .AllowFragments)
                    print("dataDownloadedAsJson = \(dataDownloadedAsJson)")
                    
                    let nameRead = dataDownloadedAsJson["name"] as? String
                    let countryRead = dataDownloadedAsJson["country"] as? String
                    let genderRead = dataDownloadedAsJson["gender"] as? String
                    let ageRead = dataDownloadedAsJson["age"] as? Int
                    
                    print("nameRead = \(nameRead!)")
                    print("countryRead = \(countryRead!)")
                    print("genderRead = \(genderRead!)")
                    print("ageRead = \(ageRead!)")
                    
                    person.name = nameRead!
                    person.age = ageRead!
                    person.gender = genderRead!
                    person.country = countryRead!
                    
                    completionHandlerPerson(responsePerson: person, errorPerson: nil)
                }
                catch
                {
                    
                }
            }
                else
                {
                    print("Error downloading data. Error = \(error)")
                    completionHandlerPerson(responsePerson: nil, errorPerson: error)

                }
            }
        
        // actually execute the task
        downloadTask.resume()
    }
}

And that should be all!
Your app should now be ready to download data in some class, and consume it on another one.

Eduardo

(it's midnight so let me know if I missed something, or something doesn't make sense)

6 comments:

  1. Hey!
    I really love your blog about coding with Swift. You rock!

    Thanks you in advance for your blog! Keep coding and writing here ;)

    ReplyDelete
  2. magnificentincense.com
    24K Monkey Classic Incense 10g
    AK-47 – X10 / PREMIUM
    Bizarro Incense
    Buy Black Mamba Incense Online
    Buy WTF Herbal Incense
    Cloud9 Mad Hatter Incense
    Crazy Monkey Incense
    k2 spray on paper
    k2 paper sheets
    Klimax Potpourri 15xxx Coconut(10g)
    Crazy Monkey Incense
    Cloud9 Mad Hatter Incense
    Buy Purple Diesel Incense Online
    Buy Pure Fire Herbal Incense Online
    Buy Kisha Cole Incense (11g) online
    Buy KUSH HERBAL INCENSE online
    Buy Mind Trip Incense Online
    Buy Platinum XXX Herbal Incense online
    buy Orange Platinum Caution 10G
    Buy OMG HERBAL POTPOURRI 10G online

    ReplyDelete
  3. https://k2incenseonlineheadshop.com/
    info@k2incenseonlineheadshop.com
    Buy liquid incense cheap
    Buy liquid incense cheap For Sale At The Best Incense Online Shop
    K2 Spice Spray | Liquid K2 | Liquid Spice | K2 Spray for sale

    ReplyDelete



  4. Our online store is one of the best in the supply of rare and high-grade (Mushrooms, Edibles. Psychedelics, and more)with absolute purity. All our products are tested to guarantee potency after production, during distribution and even storage. We ship globally in discreet and undetectable packaging using our network of trusted partners.
    psilosybin mushroom 2022, trusted magic mushroom site 2022,
    cheap liquid lsd, order cheap psychedelic mushroom,
    cheap magic mushroom, super quality shrooms,
    most potent psilocybin mushroom on the market,
    best microdosing mushrooms 2022, Golden teacher mushroom for sale,
    where to buy B+ magic mushroom, buy shrooms online ,
    buy psilocybin mushroom overnight shipping, effective microdosing for ptsd,
    buy mushroom truffles online cash on delivery, buy microdose capsules online 2021,
    magic mushrooms for mental illness for sale,
    buy mushies for anxiety/deppression and ptsd, cheap psilocybin mushroom, shrooms near me,
    where to buy psilocybin mushroom near me, cheap psychedelic mushroom for sale,
    buy caps/buttons cheap, best place to buy magic mushroom,
    buy cheap fungi mushrooms, cheap strong fungus, best quality spores for sale,
    buy mushroom grow kit online, buy fungi shrooms near me, cheapest magic mushrooms online,
    where to buy penis envy online, where to buy mexicanna online, buy microdose capsules online,
    best microdose capsule for sale,
    fungi mushroom for sale, best liquid cultures online, buy spores near me, buy Nirvana mushroom near me,
    buy pajaritos mushrooms near me, psychedelic mushroom near me, buy medicinal mushroom online,
    buy 5-MeO-DMT, buy dmt vape pen online, buy lsd tabs online, buy dmt online, where to buy dmt,
    lsd for sale, buy mushies near me, buy shrooms wholesale price, strong boomers for sale,
    Buy trippy shrooms online, buy penis envy, buy albino A+, buy amazonian cubensis,
    buy psilocybe cubensis, buy blue meanie, buy vietam cubensis, buy z strain shrooms,
    buy boomers cubensis online, buy cubensis buy golden teacher, buy liquid lsd online
    Our quality is the best you can find around and we sell in small/large quantities with guaranteed discreet delivery in good time Shipping




    website...https://megapsilocybin.com/
    call/text..+1(458) 201-6900
    email..sale@megapsilocybin.com

    ReplyDelete