Search code examples
swifttvosnsurlsessiondatatask

Nested dataTaskWithRequest in Swift tvOS


I'm a C# developer convert to Swift tvOs and just starting to learn. I've made some progress, but not sure how to handle nested calls to json. The sources are from different providers so I can't just combine the query.

How do I wait for the inner request to complete so the TVSeries has the poster_path? Is there a better way to add the show to the collection and then process the poster path loading in another thread so it doesn't delay the UI Experience?

    func downloadTVData() {
    let url_BTV = NSURL(string: BTV_URL_BASE)!
    let request_BTV = NSURLRequest(URL: url_BTV)
    let session_BTV = NSURLSession.sharedSession()

    //get series data
    let task_BTR = session_BTV.dataTaskWithRequest(request_BTV) { (data_BTV, response_BTV, error_BTV) -> Void in
        if error_BTV != nil {
            print (error_BTV?.description)
        } else {
            do {
                let dict_BTV = try NSJSONSerialization.JSONObjectWithData(data_BTV!, options: .AllowFragments) as? Dictionary<String, AnyObject>
                if let results_BTV = dict_BTV!["results"] as? [Dictionary<String, AnyObject>]{
                    for obj_BTV in results_BTV {
                        let tvshow = TVSeries(tvDict: obj_BTV)
                        //for each tv series try to load a poster_path from secondary provider
                        if let str = obj_BTV["title"] as? String!{
                            let escapedString = str?.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())!

                            if let url = NSURL(string: self.SEARCH_URL_BASE + escapedString!) {
                                let request = NSURLRequest(URL: url)
                                let session = NSURLSession.sharedSession()
                                let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
                                    if error != nil {
                                        print (error?.description)
                                    } else {
                                        do {
                                            let dict = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as? Dictionary<String, AnyObject>

                                            if let results = dict!["results"] as? [Dictionary<String, AnyObject>] {

                                                //iterate through the poster array
                                                for obj in results {
                                                    if let path = obj["poster_path"] as? String {
                                                        tvshow.posterPath = path
                                                        break
                                                    }
                                                }
                                            }
                                        } catch let error as NSError {
                                            print(error.description)
                                        }
                                    }
                                }
                                task.resume()
                            }
                        }
                        self.tvSeries.append(tvshow)
                    }
                    dispatch_async(dispatch_get_main_queue()){
                        self.collectionView.reloadData()
                    }
                }
            }  catch let error as NSError {
                print(error.description)
            }
        }
    }
    task_BTR.resume()
}

Thanks for your help!


Solution

  • I would recommend breaking things apart into multiple methods, with callbacks to sequence the operations, and utilizing Swift's built-in throws error handling mechanism. Here's an example, not perfect, but might help as a starting point:

    class TVSeries
    {
        let title: String
        var posterPath: String?
    
        enum Error: ErrorType {
            case MalformedJSON
        }
    
        init(tvDict: [String: AnyObject]) throws
        {
            guard let title = tvDict["title"] as? String else {
                throw Error.MalformedJSON
            }
    
            self.title = title
        }
    
        static func loadAllSeries(completionHandler: [TVSeries]? -> Void)
        {
            NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: BTV_URL_BASE)!) { data, response, error in
                guard let data = data else {
                    print(error)
                    completionHandler(nil)
                    return
                }
    
                do {
                    completionHandler(try fromJSONData(data))
                }
                catch let error {
                    print(error)
                }
            }.resume()
        }
    
        static func fromJSONData(jsonData: NSData) throws -> [TVSeries]
        {
            guard let dict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as? [String: AnyObject] else {
                throw Error.MalformedJSON
            }
    
            guard let results = dict["results"] as? [[String: AnyObject]] else {
                throw Error.MalformedJSON
            }
    
            return try results.map {
                return try TVSeries(tvDict: $0)
            }
        }
    
        func loadPosterPath(completionHandler: () -> Void)
        {
            guard let searchPath = title.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet()) else {
                completionHandler()
                return
            }
    
            let url = NSURL(string: SEARCH_URL_BASE)!.URLByAppendingPathComponent(searchPath)
            NSURLSession.sharedSession().dataTaskWithURL(url) { [weak self] data, response, error in
                defer { completionHandler() }
    
                guard let strongSelf = self else { return }
    
                guard let data = data else {
                    print(error)
                    return
                }
    
                do {
                    strongSelf.posterPath = try TVSeries.posterPathFromJSONData(data)
                }
                catch let error {
                    print(error)
                }
            }.resume()
        }
    
        static func posterPathFromJSONData(jsonData: NSData) throws -> String?
        {
            guard let dict = try NSJSONSerialization.JSONObjectWithData(jsonData, options: .AllowFragments) as? [String: AnyObject] else {
                throw Error.MalformedJSON
            }
    
            guard let results = dict["results"] as? [[String: AnyObject]] else {
                throw Error.MalformedJSON
            }
    
            for result in results {
                if let path = result["poster_path"] as? String {
                    return path
                }
            }
    
            return nil
        }
    }
    

    It might also be worth your time to look into something like RxSwift or Alamofire, which help you with these kinds of data-conversion / sequencing operations.