Xcoding with Alfian

Mobile development articles and tutorials

Encapsulate iOS & Mac Asynchronous and Dependent Tasks into Cocoa Operation Subclass

Alt text

Asynchronous tasks like fetching data from network, parsing, processing, and saving data to local cache are routine tasks apps nowaday perform. As a developer, we have to make sure the UI/main thread runs smoothly and move the long running heavy workload tasks into background thread to maintain 60 FPS animation.

Apple provides 2 ways for developers to perform tasks in background thread:

  1. Grand Central Dispatch (GCD): Set of API for developers to perform tasks serially or concurrently in background thread pool using queue.
  2. Operation (also known as NSOperation): A Cocoa Abstract Class that represents single unit of task to perform. It is a thread safe class with built in state, priority and QoS, cancellation, and dependencies management out of the box.

In this article we are going to build an asynchronous Operation Subclass that fetch repositories from GitHub API asynchronously, and a dependent Operation subclass that parse and serialize the fetch repository data into Swift Class.

What we will build

Here are the things that we will build for this article:

  1. AsynchronousOperation: an Operation Subclass that support asynchronous operation.
  2. FetchRepoOperation: AsynchronousOperation Subclass that fetch the Data for latest trending GitHub repositories since last week using URLSession asynchronously.
  3. ParseRepoDataOperation: Operation Subclass that decode and serialize the Data from FetchRepoOperation into an array of GithubRepo object using Swift Codable and JSONDecoder.
  4. Playground Page: Perform the operations using OperationQueue, add dependency between operation, and passing the data between operation objects using the completion block.

Implementing Asynchronous Operation using Operation Subclass

By default, Operation Class runs the code synchronously. Apple provides a way to run code asynchronously by subclassing and overriding isAsynchronous boolean property to true.

We also need to add our own state management property using enumeration, handle the change of state from ready, executing, and finished. dispacth queue will be used to handle synchronization using dispatch barrier for the state property when the property is being read and written concurrently.

In the start function we check if the task is not cancelled, if cancelled we just call finish to change the state to finished and return. If not we set the state to executing and call the main function. The main function will be overriden by our subclass to perform the tasks inside the function.

/* https://stackoverflow.com/questions/43561169/trying-to-understand-asynchronous-operation-subclass */

public class AsynchronousOperation: Operation {
    @objc private enum State: Int {
        case ready
        case executing
        case finished
    }
    
    private var _state = State.ready
    private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".op.state", attributes: .concurrent)
    
    @objc private dynamic var state: State {
        get { return stateQueue.sync { _state } }
        set { stateQueue.sync(flags: .barrier) { _state = newValue } }
    }
    
    public override var isAsynchronous: Bool { return true }
    open override var isReady: Bool {
        return super.isReady && state == .ready
    }
    
    public override var isExecuting: Bool {
        return state == .executing
    }
    
    public override var isFinished: Bool {
        return state == .finished
    }
    
    open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        if ["isReady",  "isFinished", "isExecuting"].contains(key) {
            return [#keyPath(state)]
        }
        return super.keyPathsForValuesAffectingValue(forKey: key)
    }
    
    public override func start() {
        if isCancelled {
            finish()
            return
        }
        self.state = .executing
        main()
    }
    open override func main() {
        fatalError("Implement in sublcass to perform task")
    }
    
    public final func finish() {
        if isExecuting {
            state = .finished
        }
    }
}

Implementing FetchRepoOperation to Fetch Github API

The FetchRepoOperation is a subclass of Asynchronous Operation, we declare two optional properties, fetchedData which is a Data object that will be used to store the data response from the API call, and an error property that will be used to store the error from an API call if it occurs.

Inside the overriden main method from the superclass, we construct the URL and the query items, the query items will query the repositories created since last week ordered descending by stars count. After that we initialize the URLRequest and invoke asynchronous data task with URLSession.

Inside the data task completion handler, we assign the response data and error to the instance properties and call finish method to set the state of the operation to finished to mark the operation as completed.

public class FetchRepoOperation: AsynchronousOperation {
    
    var urlSession = URLSession.shared
    var fetchedData: Data?
    var error: Error?
    
    public override func main() {
        let lastWeekDate = Calendar.current.date(byAdding: .weekOfYear, value: -1, to: Date())!
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        let lastWeekDateString = dateFormatter.string(from: lastWeekDate)
        
        var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")!
        urlComponents.queryItems = [
            URLQueryItem(name: "q", value: "created:>\(lastWeekDateString)"),
            URLQueryItem(name: "sort", value: "stars"),
            URLQueryItem(name: "order", value: "desc"),
            URLQueryItem(name: "page", value: "0"),
            URLQueryItem(name: "per_page", value: "25")
        ]

        let urlRequest = URLRequest(url: urlComponents.url!)        
        urlSession.dataTask(with: urlRequest) { [weak self](data, response, error) in
            self?.error = error
            self?.fetchedData = data
            self?.finish()
            }.resume()

    }
}

Implementing ParseRepoOperation to decode JSON Data with Swift Codable Class

We create GithubRepoFetchResult, GithubRepo, GithubOwner Swift Class that implements codable and CodingKeys enum to map the json property name to the instance property camel case name. By using Codable, we can utilize JSONDecoder to decode the Data to the class that implements Codable automatically.

public class GithubRepoFetchResult: Codable {
    public var items: [GithubRepo] = []
}

public class GithubRepo: Codable {
    
    public var name: String? = ""
    public var fullName: String? = ""
    public var owner: GithubRepoOwner? = nil
    public var stargazersCount = 0
    public var desc: String? = ""
    public var url: URL? = nil
    
    enum CodingKeys: String, CodingKey {
        case name
        case fullName = "full_name"
        case owner
        case stargazersCount = "stargazers_count"
        case desc = "description"
        case url
    }
}

public class GithubRepoOwner:  Codable {
    public var login: String? = ""
}

Implementing ParseRepoOperation is very straightforward, we use Operation as subclass because JSONDecoder decode function is synchronous so we don’t need to use AsynchronousOperation.

We declare 3 optional instance properties, fetchedData is Data passed from the FetchRepoOperation, error is an Error object in case an error occurs when decoding the Data into object, the repos array containing GitHubRepo that will be used to store the result of the JSONDecoding into object.

Inside the main function we use guard to unwrap the optional fetchedData, if it is nil we just return from the function. After that inside the try catch block we use JSONDecoder decode function passing the fetchedData and the GithubRepoFetchResult as the root class to decode. Then, we assign the the items property from the GithubRepoFetchResult into the repos instance property. In case of error occurs when decoding, we assign the error to our error instance property.

public class ParseRepoOperation: Operation {
    var fetchedData: Data?
    var repos: [GithubRepo]?
    var error: Error?
    
    public override func main() {
        guard let fetchedData = fetchedData else { 
            return
        }
        
        do {
            let githubRepoResult = try JSONDecoder().decode(GithubRepoFetchResult.self, from: fetchedData)
            self.repos = githubRepoResult.items
        } catch  {
            self.error = error
        } 
    }
}

Performing the Operations using OperationQueue

To perform the operations we use OperationQueue that acts a priority queue that handle the execution of the operations using First In First Out mechanism. We set maxConcurrentOperationCount to 1 so our operations does not perform concurrently at the same time.

We instantiate the FetchRepoOperation and ParseRepoOperation object, then we add FetchRepoOperation object as the dependency for ParseRepoOperation object so the fetch task will be started first and the must be finished for the parse task to start.

Passing data between operations is not straightforward, there are many ways to do it such as using Data wrapper reference class containing the data and then pass it to each operation. For this implementation, we will use the Operation completion block that will be called when the operation is finished. We assign the fetch operation completion block a closure with reference to parse and fetch operation objects. Unowned is used to avoid the retain cycle, inside the block we passed the fetch response data to the parse fetchedData property.

We assign the parse operation completion block property a closure that just loop the repos and print the name of the repo to the console so we can see the result.

At last to kick off the operations, we invoke the OperationQueue addOperations passing array containing the fetch and parse operations to begin the tasks.

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1

let githubFetchOperation = FetchLatestTrendingRepoInGitHubOperation()
let githubParseOperation = ParseFetchGithubRepoJSONDataOperation()
githubParseOperation.addDependency(githubFetchOperation)

githubFetchOperation.completionBlock = { [unowned githubFetchOperation, unowned githubParseOperation] in
    githubParseOperation.fetchedData = githubFetchOperation.fetchedData
}

githubParseOperation.completionBlock = { [unowned githubParseOperation] in
    githubParseOperation
    githubParseOperation.repos?.forEach { print($0.name) }
}

operationQueue.addOperations([githubFetchOperation, githubParseOperation], waitUntilFinished: false)

Conclusion

Cocoa Operation Class provides developer great flexibilities such as dependencies between task, adjust the queue priority and QoS, cancellation and state management when performing background tasks. It is a great tool to use as an iOS developer just like the Grand Central Dispatch(GCD). Using them effectively will provide the result of silky smooth and high performant iOS/Mac apps.