简体   繁体   中英

How to make a JSON Response asynchronous in Swift with NSOperationQueue and NSBlockOperation?

I would like to make only the JSON Response asynchronous with mentioned classes. I specifically want to use the method I have in my NSOperation class. This seems very complex for me, because I don't know where the parameters should come from for my method 'parseResponse'. Here is my attempt + the relevant code.

I apprecciate any kind of help.

Edited the code and the comments to clear things up. I know that my old code is already asynchronous but I want to rewrite it so the new solution is with NSOperationQueue and NSBlockOperation.

func jsonParser() {
    let urlPath = "https://api.github.com/users/\(githubName)/repos"

    guard let endpoint = NSURL(string: urlPath) else {
        print("Error creating endpoint")
        let alert = UIAlertController(title: "Error Github Name Request", message: "Error creating endpoint", preferredStyle: UIAlertControllerStyle.Alert)
        let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) {
            UIAlertAction in
            self.navigationController?.popToRootViewControllerAnimated(true)
        }
        alert.addAction(okAction)
        self.presentViewController(alert, animated: true, completion: nil);

        return
    }


   // How would I implement this part of the code, to make the response asynchronous? 

 let queue = NSOperationQueue()
 let o = JSONParser()



var blockOperation = NSBlockOperation { () -> Void in

    NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in

        o.parseResponse(data, response: response, error: error, completionHandler: (parsedData: [ViewControllerTableView.Repository], error: NSError) -> () )


        }.resume()
    }



queue.addOperation(blockOperation)




// The commented code below is my old solution. And I want to take my old
 //solution and rewrite it so it actually uses NSOperationQueue and 

// NSBlockOperation 

/*
    let request = NSMutableURLRequest(URL:endpoint)
    NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) in
        do {
            guard let data = data else {
                let alert = UIAlertController(title: "Error Github Name Request", message: "\(JSONError.NoData)", preferredStyle: UIAlertControllerStyle.Alert)

                let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) {
                    UIAlertAction in
                    self.navigationController?.popToRootViewControllerAnimated(true)
                }

                alert.addAction(okAction)
                self.presentViewController(alert, animated: true, completion: nil);
                throw JSONError.NoData
            }

            guard let json = try NSJSONSerialization.JSONObjectWithData(data, options:.AllowFragments) as? [[String: AnyObject]] else {

                let alert = UIAlertController(title: "Error Github Name Request", message: "\(JSONError.ConversionFailed)", preferredStyle: UIAlertControllerStyle.Alert)

                let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default) {
                    UIAlertAction in
                    self.navigationController?.popToRootViewControllerAnimated(true)
                }
                alert.addAction(okAction)
                self.presentViewController(alert, animated: true, completion: nil);
                throw JSONError.ConversionFailed
            }

            self.owner=json[0]["owner"]!["login"]! as! String
            self.allRepos.removeAll()
            for a in json {
                let temp:Repository = Repository(id: (a["id"] as? Int)!,name: (a["name"] as? String), size: (a["size"] as? Int)!, watchers: (a["watchers"] as? Int), created_at: (a["created_at"] as? String)!, descr: (a["description"] as? String)!)
                self.allRepos.append(temp)
            }
            self.tableRefresh(self.allRepos)

        } catch let error as JSONError {
            print(error.rawValue)
        } catch let error as NSError {
            print(error.debugDescription)
        }
        }.resume()
*/
}



class JSONParser {


    func parseResponse(data: NSData?, response:
        NSURLResponse?,error:
        NSError?,completionHandler: (parsedData:
        [Repository],error: NSError) -> ())
    {


    }

}

In order to wrap an asynchronous NSURLSession task in an operation, you have to use an asynchronous, custom NSOperation subclass. Unfortunately, NSBlockOperation , and even the standard NSOperation subclasses, are designed to handle synchronous tasks (ie the operation finishes as soon as the synchronous task finishes).

But in this case, we're dealing with an asynchronous task. So, we have to subclass NSOperation and explicitly tell it that it is asynchronous. Furthermore, we have to engage in special KVO of isExecuting and isFinished , for this asynchronous behavior to work properly. For more information, see Configuring Operations for Concurrent Execution in the Concurrency Programming Guide.

Personally, I encompass that in an abstract AsynchronousOperation class:

/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
///   necessary and then ensuring that `completeOperation()` is called; or
///   override `cancel` method, calling `super.cancel()` and then cleaning-up
///   and ensuring `completeOperation()` is called.

public class AsynchronousOperation : NSOperation {

    override public var asynchronous: Bool { return true }

    private let stateLock = NSLock()

    private var _executing: Bool = false
    override private(set) public var executing: Bool {
        get {
            return stateLock.withCriticalScope { _executing }
        }
        set {
            willChangeValueForKey("isExecuting")
            stateLock.withCriticalScope { _executing = newValue }
            didChangeValueForKey("isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var finished: Bool {
        get {
            return stateLock.withCriticalScope { _finished }
        }
        set {
            willChangeValueForKey("isFinished")
            stateLock.withCriticalScope { _finished = newValue }
            didChangeValueForKey("isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func completeOperation() {
        if executing {
            executing = false
        }

        if !finished {
            finished = true
        }
    }

    override public func start() {
        if cancelled {
            finished = true
            return
        }

        executing = true

        main()
    }

    override public func main() {
        fatalError("subclasses must override `main`")
    }
}

extension NSLock {

    /// Perform closure within lock.
    ///
    /// An extension to `NSLock` to simplify executing critical code.
    ///
    /// - parameter block: The closure to be performed.

    func withCriticalScope<T>(@noescape block: Void -> T) -> T {
        lock()
        let value = block()
        unlock()
        return value
    }
}

Once you have that, making an operation that performs a network request is quite simple:

/// Simple network data operation
///
/// This can be subclassed for image-specific operations, JSON-specific operations, etc.

class DataOperation : AsynchronousOperation {
    var request: NSURLRequest
    var session: NSURLSession
    weak var downloadTask: NSURLSessionTask?
    var networkCompletionHandler: ((NSData?, NSURLResponse?, NSError?) -> ())?

    init(session: NSURLSession, request: NSURLRequest, networkCompletionHandler: (NSData?, NSURLResponse?, NSError?) -> ()) {
        self.session = session
        self.request = request
        self.networkCompletionHandler = networkCompletionHandler

        super.init()
    }

    override func main() {
        let task = session.dataTaskWithRequest(request) { data, response, error in
            self.networkCompletionHandler?(data, response, error)
            self.completeOperation()
        }
        task.resume()
        downloadTask = task
    }

    override func cancel() {
        super.cancel()

        downloadTask?.cancel()
    }

    override func completeOperation() {
        networkCompletionHandler = nil

        super.completeOperation()
    }
}

And if you want an operation that parses your JSON, you just subclass that:

class GitRepoOperation: DataOperation {
    typealias ParseCompletionHandler = (String?, [Repository]?, ErrorType?) -> ()

    let parseCompletionHandler: ParseCompletionHandler

    init(session: NSURLSession, githubName: String, parseCompletionHandler: ParseCompletionHandler) {
        self.parseCompletionHandler = parseCompletionHandler
        let urlPath = "https://api.github.com/users/\(githubName)/repos"
        let url = NSURL(string: urlPath)!
        let request = NSURLRequest(URL: url)
        super.init(session: session, request: request) { data, response, error in
            guard let data = data where error == nil else {
                parseCompletionHandler(nil, nil, JSONError.NoData)
                return
            }

            do {
                guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [[String: AnyObject]] else {
                    parseCompletionHandler(nil, nil, JSONError.ConversionFailed)
                    return
                }

                guard let owner = json.first?["owner"]?["login"] as? String else {
                    parseCompletionHandler(nil, nil, JSONError.MissingFields)
                    return
                }

                var repos = [Repository]()
                for a in json {
                    guard let id = a["id"] as? Int, let size = a["size"] as? Int, let createdAt = a["created_at"] as? String, let descr = a["description"] as? String else {
                        parseCompletionHandler(nil, nil, JSONError.MissingFields)
                        return
                    }

                    repos.append(Repository(id: id, name: a["name"] as? String, size: size, watchers: a["watchers"] as? Int, created_at: createdAt, descr: descr))
                }
                parseCompletionHandler(owner, repos, nil)
            } catch {
                parseCompletionHandler(nil, nil, JSONError.ConversionFailed)
            }
        }
    }
}

Note, I have excised those forced unwrapping operators ( ! ) and instead do optional binding to gracefully detect missing fields (for the non-optional ones, at least). I also added an additional error type, so the caller could differentiate between different types of errors. But, bottom line, you might want to use the ! operator more judiciously.

And to call that, you just do something like:

let operation = GitRepoOperation(session: session, githubName: name) { owner, repos, error in
    guard let owner = owner, let repos = repos where error == nil else {
        // handle the error however you want here
        return
    }

    // handle `owner` and `repos` however you want here
}
queue.addOperation(operation)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM