Search code examples
jsonswiftnsoperationqueue

How to download and parse JSON with OperationQueue in Swift


I have gotten stuck on a conceptually simple problem. What's happening is that the parse operation is executing before the downloading operation's completion handler finishes. As a result there is no data to parse. You can drop the following code right into a file and run it.

How do i make sure downloading completes before the parse operation runs?

import UIKit

let search = "https://api.nal.usda.gov/ndb/search/?format=json&q=butter&sort=n&max=25&offset=0&api_key=DEMO_KEY"

class ViewController: UIViewController {



    override func viewDidLoad() {
        super.viewDidLoad()

        let fetch = FetchNBDNumbersOperation()
        let parse = NDBParseOperation()

        // 1
        let adapter = BlockOperation() { [unowned parse, unowned fetch] in
            parse.data = fetch.data
        }

        // 2
        adapter.addDependency(fetch)
        parse.addDependency(adapter)

        // 3
        let queue = OperationQueue()
        queue.addOperations([fetch, parse, adapter], waitUntilFinished: true)
    }
}

class FetchNBDNumbersOperation: Operation {

    var data: Data?

    override func main() {
        let url = URL(string: search)!
        let urlSession = URLSession.shared
        let dataTask = urlSession.dataTask(with: url) { (jsonData, response, error) in
            guard let jsonData = jsonData, let response = response else {
                debugPrint(error!.localizedDescription)
                return
            }
            self.data = jsonData
            print("Response URL: \(String(describing: response.url?.absoluteString))")
        }
        dataTask.resume()
    }
}

class NDBParseOperation: Operation {

    var data: Data?
    var nbdNumbers = [NBDNumber]()

    override func main() {
        let decoder = JSONDecoder()
        do {
            guard let jsonData = self.data else {
                fatalError("No Data")
            }
            let dictionary = try decoder.decode( [String: USDAFoodSearch].self, from: jsonData )
            for (_, foodlist) in dictionary {
                for food in foodlist.item {
                    print("\(food.name) \(food.ndbno) \(food.group)")
                    let nbdNumber = NBDNumber(name: food.name, nbdNo: food.ndbno)
                    nbdNumbers.append(nbdNumber)
                }
            }
        } catch {
            print(error.localizedDescription)
        }
    }
}

struct NBDNumber {
    var name: String
    var nbdNo: String
}

struct USDAFoodSearch: Decodable {
    let q: String
    let sr: String
    let ds: String
    let start: Int
    let end: Int
    let total: Int
    let group: String
    let sort: String
    let item: [USDAFood]

    struct USDAFood: Decodable {
        let offset: Int     //Position in Array
        let group: String
        let name: String
        let ndbno: String
        let ds: String
    }
}

Solution

  • Here's the answer. Subclass Fetch Operation with the class below. And the tell it the operation is completed at the end of the Fetch Op completion handler.

    class FetchNBDNumbersOperation: AsynchronousOperation {
    
        var data: Data?
    
        override func main() {
            super.main()
    
            let url = URL(string: search)!
            let urlSession = URLSession.shared
            let dataTask = urlSession.dataTask(with: url) { (jsonData, response, error) in
                guard let jsonData = jsonData, let response = response else {
                    debugPrint(error!.localizedDescription)
                    return
                }
                self.data = jsonData
                print("Response URL: \(String(describing: response.url?.absoluteString))")
                self.state = .finished
            }
            dataTask.resume()
        }
    }
    

    Found the Asynchronous Subclass here: https://gist.github.com/Sorix/57bc3295dc001434fe08acbb053ed2bc

    /// Subclass of `Operation` that add support of asynchronous operations.
    /// ## How to use:
    /// 1. Call `super.main()` when override `main` method, call `super.start()` when override `start` method.
    /// 2. When operation is finished or cancelled set `self.state = .finished`
    class AsynchronousOperation: Operation {
        override var isAsynchronous: Bool { return true }
        override var isExecuting: Bool { return state == .executing }
        override var isFinished: Bool { return state == .finished }
    
        var state = State.ready {
            willSet {
                willChangeValue(forKey: state.keyPath)
                willChangeValue(forKey: newValue.keyPath)
            }
            didSet {
                didChangeValue(forKey: state.keyPath)
                didChangeValue(forKey: oldValue.keyPath)
            }
        }
    
        enum State: String {
            case ready = "Ready"
            case executing = "Executing"
            case finished = "Finished"
            fileprivate var keyPath: String { return "is" + self.rawValue }
        }
    
        override func start() {
            if self.isCancelled {
                state = .finished
            } else {
                state = .ready
                main()
            }
        }
    
        override func main() {
            if self.isCancelled {
                state = .finished
            } else {
                state = .executing
            }
        }
    }