Sunday, 10 July 2016

Protocol oriented loading of resources from a network service in Swift


Update 12-December-2016: TABResourceLoader was created using this pattern and is production ready.

TL;DR - Define the resources in your application by conforming to protocols that define where and how to get them using a generic service type. Finally make your operations conform to a protocol to use them to retrieve your resources
With the addition of protocol extensions in Swift 2.0 it's now possible to focus less on subclassing and define your interfaces with default implementations on a protocol extension. This concept is used extensibly in the approach I currently use to a load resources into an iOS app. The sample code on this post can be downloaded from Github.

Defining a Resource

A resource can be defined as something that you application will consume. For example, it could be a JSON or XML file, an image, a video, etc. In Swift this can be represented using a protocol with an associated type:
public protocol ResourceType {
  associatedtype Model
}
The Model would be the strong type used to represent that data in your code. Specifically, we can then define a JSON resource. It conforms ResourceType and contains two parsing functions, a way of retrieving the generic Model from a JSON dictionary and a way of parsing it from a JSON array.
public protocol JSONResourceType: ResourceType {
  func modelFrom(jsonDictionary jsonDictionary: [String : AnyObject]) -> Model?
  func modelFrom(jsonArray jsonArray: [AnyObject]) -> Model?
}
Naturally for most cases a resource will come from either a dictionary or an array so I use a protocol extension to add default implementations of this functions to return nil. When you create a new type that conforms to JSONResourceTypeyou should provide an implementation to either of these functions.
extension JSONResourceType {
  public func modelFrom(jsonDictionary jsonDictionary: [String : AnyObject]) -> Model? { return nil }
  public func modelFrom(jsonArray jsonArray: [AnyObject]) -> Model? { return nil }
}
The following step is to transform some NSData into either a JSON dictionary (i.e. [String: AnyObject]) or a JSON array (i.e. [AnyObject]). This is done using the commonly known NSJSONSerialization.JSONObjectWithData. As a side note, I prefer to use a Result type as the return type since this works very well for operations where the return type is different depending whether the function succeeds or fails:
public enum Result<T> {
  case Success(T)
  case Failure(ErrorType)
}
enum ParsingError: ErrorType {
  case InvalidJSONData
  case CannotParseJSONDictionary
  case CannotParseJSONArray
  case UnsupportedType
}
extension JSONResourceType {
  
  func resultFrom(data data: NSData) -> Result<Model> {
    guard let jsonObject = try? NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers) else {
      return .Failure(ParsingError.InvalidJSONData)
    }
    
    if let jsonDictionary = jsonObject as? [String: AnyObject] {
      return resultFrom(jsonDictionary: jsonDictionary)
    }
    
    if let jsonArray = jsonObject as? [AnyObject] {
      return resultFrom(jsonArray: jsonArray)
    }
    
    // This is likely an impossible case since `JSONObjectWithData` likely only returns [String: AnyObject] or [AnyObject] but still needed to appease the compiler
    return .Failure(ParsingError.UnsupportedType)
  }
  
  private func resultFrom(jsonDictionary jsonDictionary: [String: AnyObject]) -> Result<Model> {
    if let parsedResults = modelFrom(jsonDictionary: jsonDictionary) {
      return .Success(parsedResults)
    } else {
      return .Failure(ParsingError.CannotParseJSONDictionary)
    }
  }
  
  private func resultFrom(jsonArray jsonArray: [AnyObject]) -> Result<Model> {
    if let parsedResults = modelFrom(jsonArray: jsonArray) {
      return .Success(parsedResults)
    } else {
      return .Failure(ParsingError.CannotParseJSONArray)
    }
  }
  
}
At this point we can load any JSONResourceType from some NSData, so the next step is to get the data from somewhere. For the sake of keeping an already long post short, I'm only going to show how to retrieve it from a web service.
Before diving into the networking logic I'll introduce another protocol to define the necessary values to retrieve a JSONResourceType from the network. In most scenarios the following 4 parameters will be sufficient to download the resource.
public protocol NetworkResourceType {
  var url: NSURL { get }
  var HTTPMethod: String { get }
  var allHTTPHeaderFields: [String: String]? { get }
  var JSONBody: AnyObject? { get }
}
In a lot of cases only the URL is necessary to know where the JSON file is so we can add default values for the other three values:
public extension NetworkResourceType {
  var HTTPMethod: String { return "GET" }
  var allHTTPHeaderFields: [String: String]? { return nil }
  var JSONBody: AnyObject? { return nil }
}
The final step is to add the ability to generate a NSURLRequest from these parameters:
extension NetworkResourceType {
  func urlRequest() -> NSURLRequest {
    let request = NSMutableURLRequest(URL: url)
    request.allHTTPHeaderFields = allHTTPHeaderFields
    request.HTTPMethod = HTTPMethod
    
    if let body = JSONBody {
      request.HTTPBody = try? NSJSONSerialization.dataWithJSONObject(body, options: NSJSONWritingOptions.PrettyPrinted)
    }
    
    return request
  }
}
At this point we can define a NetworkJSONResource by combining NetworkResource and JSONResource:
public protocol NetworkJSONResourceType: NetworkResourceType, JSONResourceType {}
By conforming to NetworkJSONResourceType we know where the location of the JSON response is and how to map it into an app-specific data type.

Downloading the resource from the network

Now that we know how a NetworkJSONResourceType looks it's time to download it! This is done using the NSURLSession.sharedSession. The first thing to do is to declare a protocol that defines how a resource service looks like. It has 2 functions, one for initialization and another for fetching the resource with a completion handler:
public protocol ResourceServiceType {
  associatedtype Resource: ResourceType
  init()
  func fetch(resource resource: Resource, completion: (Result<Resource.Model>) -> Void)
}
Then we can create a NetworkJSONService that conforms to this ResourceServiceType to download the resource. 
As an aside, before diving into the NetworkJSONService I define a URLSessionType protocol that wraps NSURLSession's dataTaskWithRequest method, so that I can then use dependency injection to test the service by passing in a URLSessionType type.
protocol URLSessionType {
  func perform(request request: NSURLRequest, completion: (NSData?, NSURLResponse?, NSError?) -> Void)
}

extension NSURLSession: URLSessionType {
  public func perform(request request: NSURLRequest, completion: (NSData?, NSURLResponse?, NSError?) -> Void) {
    dataTaskWithRequest(request, completionHandler: completion).resume()
  }
}
The implementation of the NetworkJSONService service is very simple. In the fetch(resource:completion:)method from ResourceServiceType the URLSessionType performs a request and calls the completion handler with the Result. The handling of the response is done in the private resultFrom(resource data:URLResponse:error:) method. At this point the usual checking for errors is carried out (error checking could be more exhaustive). Finally the JSONResourceType method defined previously to convert the NSData onto the needed model is invoked.
enum NetworkJSONServiceError: ErrorType {
  case NetworkingError(error: NSError)
  case NoData
}

public struct NetworkJSONService<Resource: NetworkJSONResourceType> {
  
  private let session: URLSessionType
  
  init(session: URLSessionType) {
    self.session = session
  }
  
  private func resultFrom(resource resource: Resource, data: NSData?, URLResponse: NSURLResponse?, error: NSError?) -> Result<Resource.Model> {
    if let error = error {
      return .Failure(NetworkJSONServiceError.NetworkingError(error: error))
    }
    
    guard let data = data else {
      return .Failure(NetworkJSONServiceError.NoData)
    }
    
    return resource.resultFrom(data: data)
  }
  
}
// MARK: - ResourceServiceType
extension NetworkJSONService: ResourceServiceType {
  
  public init() {
    self.init(session: NSURLSession.sharedSession())
  }
  
  public func fetch(resource resource: Resource, completion: (Result<Resource.Model>) -> Void) {
    let urlRequest = resource.urlRequest()
    
    session.perform(request: urlRequest) { (data, _, error) in
      completion(self.resultFrom(resource: resource, data: data, URLResponse: nil, error: error))
    }
  }
  
}
That's pretty much all that is needed to have a very flexible and testable networking stack where all the endpoints called are defined as resources.

Using an operation to retrieve the data from the network

If you use NSOperations, this pattern fits very well with them. If you don't know much about them I would highly encourage watching the Advanced Operations WWDC 2016 video and checking out the sample code.
From this point I'll assume that you have a basic understanding of how to use operations. The first thing to do is define two protocols, Cancellable to know whether the operation has been cancelled and Finishable to be called when the work in the operation has been completed. An NSOperation will conform to the first protocol, so no extra work is needed to conform to it, the second one is inspired by the Operation class in the Advanced Operations sample code where the subclass of the operation must call finish with some errors if any exist when the work is complete.
public protocol Cancellable: class {
  var cancelled: Bool { get }
}

public protocol Finishable: class {
  func finish(errors: [NSError])
}
The next protocol defines what a ResourceOperationType should do. This conforms to both Cancellable and Finishable, and it include an associated type that conforms to the ResourceType protocol defined previously. The other requirements are a method to fetch the associatedtype Resource using a ResourceServiceType and a method that will be called when the operation completes providing the Result of it.
public protocol ResourceOperationType: Cancellable, Finishable {
  associatedtype Resource: ResourceType
  func fetch<Service: ResourceServiceType where Service.Resource == Resource>(resource resource: Resource, usingService service: Service)
  func didFinishFetchingResource(result result: Result<Resource.Model>)
}
Noticeably, the ResourceOperationType protocol does not provide a definition of where the Resource needs to be retrieved from, it could come form somewhere in the disk, the cloud, etc. Only it defines that it can be retrieved using a ResourceServiceType. To avoid having to write the code that fetches the resource using a service on each operation a function can be implemented on an extension on ResourceOperationType to be called in the method where the operation does it's work. (For NSOperations this can be called on the main() function).
public extension ResourceOperationType {
  
  public func fetch<Service: ResourceServiceType where Service.Resource == Resource>(resource resource:Resource, usingService service: Service) {
    if cancelled { return }
    service.fetch(resource: resource) { [weak self] (result) in
      NSThread.executeOnMain { [weak self] in
        guard let strongSelf = self else { return }
        if strongSelf.cancelled { return }
        strongSelf.finish([])
        strongSelf.didFinishFetchingResource(result: result)
      }
    }
  }
  
}
That's it! A few protocol definitions, some enums and one struct later we can create our operations that conform to ResourceOperationType and use a NetworkJSONService to download our JSON in a concurrent way.

A concrete example

After writing a lot of code and explaining the various patterns of this architecture it's time to share a concrete example to see how this works in the real world. Let's say we are downloading a list of cities from some web service where the JSON response looks like this:
{
  "cities": [{
    "name": "Paris"
  }, {
    "name": "London"
  }]
}
The model in our application can be represented by the following struct:
struct City {
  let name: String
}
The parsing can be done in an extension where the City can be initialised from a JSON dictionary. (Note there are simpler ways to write the parsing logic, check out JSONUtilities for an example)
extension City {
  init?(jsonDictionary: [String : AnyObject]) {
    guard let parsedName = jsonDictionary["name"] as? String else {
      return nil
    }
    name = parsedName
  }
}
After creating the model we need to create the CitiesResource where we define how to decode the 'cities' key into an array of JSON dictionary cities.
// This base endpoint would probably be defined somewhere globally
let baseURL = NSURL(string: "http://localhost:8000/")!

struct CitiesResource: NetworkJSONResourceType {
  typealias Model = [City]
  
  let url: NSURL
    
  init(continent: String) {
    url = baseURL.URLByAppendingPathComponent("\(continent).json")
  }
  
  //MARK: JSONResource
  func modelFrom(jsonDictionary jsonDictionary: [String: AnyObject]) -> [City]? {
    guard let
      citiesJSONArray: [[String: AnyObject]] = jsonDictionary["cities"] as? [[String: AnyObject]]
      else {
        return []
    }
    return citiesJSONArray.flatMap(City.init)
  }
  
}
Once all the information is defined about how to parse the cities endpoint into an array of cities (i.e. [City]) it's time to download them. To do this I have BaseOperation which is a subclass of NSOperation where I have all the finished, executing and canceled KVO boilerplate (see the implementation on Github). Nothing fancy, the only thing needed to know is that the work is carried out on the execute() function. Again I highly encourage to watch the Advanced Operations WWDC video as this BaseOperation can be replaced by the Operation Apple provides in the sample code. 
The operation is initialised with a continent (maybe something that the user selected from a table) to be able to create the CitiesResource. To notify about the completion of the operation I use a completion handler containing the generic operation and result. 
final class ResourceOperation<ResourceService: ResourceServiceType>: BaseOperation, ResourceOperationType {
  
  typealias Resource = ResourceService.Resource
  typealias DidFinishFetchingResourceCallback = (ResourceOperation<ResourceService>, Result<Resource.Model>) -> Void

  private let resource: Resource
  private let service: ResourceService  
  private let didFinishFetchingResourceCallback: DidFinishFetchingResourceCallback
  
  init(resource: ResourceService.Resource, service: ResourceService = ResourceService(), didFinishFetchingResourceCallback: DidFinishFetchingResourceCallback) {
    self.resource = resource
    self.service = service
    self.didFinishFetchingResourceCallback = didFinishFetchingResourceCallback
    super.init()
  }
  
  override func execute() {
    fetch(resource: resource, usingService: service)
  }
  
  func didFinishFetchingResource(result result: Result<Resource.Model>) {
    didFinishFetchingResourceCallback(self, result)
  }
  
}
The definition of the operation for the CitiesResource that uses a NetworkJSONService can be declared in a typealias
typealias CitiesNetworkResourceOperation = ResourceOperation<NetworkJSONService<CitiesResource>>
At this point the networking stack is complete and can be used wherever it makes sense in your application. For the sake of simplicity we'll put in a view controller following the commonly used MVC pattern (i.e. Massive View Controller):
class ViewController: UIViewController {

  private let operationQueue = NSOperationQueue()

  override func viewDidLoad() {
    super.viewDidLoad()    
    let americaResource = CitiesResource(continent: "america")
    let citiesNetworkResourceOperation = CitiesNetworkResourceOperation(resource: americaResource) { [weak self] operation, result in
      if operation.cancelled { return }
      // Handle result if operation not cancelled
    }
    operationQueue.addOperation(citiesNetworkResourceOperation)
  }
     
}

Conclusions

There is very little repetition in the components built with this architecture. It's very easy to test, everything in this post apart from the ViewController can be easily unit tested (mainly through dependency injection). The components are small and easy to reason about, meaning onboarding of new developers is simpler. The ownership of each object is simple, so once the operation is torn down, the whole stack is removed from memory. Finally, adding this pattern to a current architecture is straight forward because each resource has it's own networking stack independent of the others.
To see how this pattern can be use to load a JSON file from disk see the Github repository where the code for LoadItcan be found.

92 comments:

  1. Would love to see a rewrite of this using Firebase and the Codable protocol

    ReplyDelete
  2. I feel really happy to have seen your webpage and look forward to so
    many more entertaining times reading here. Thanks once more for all
    the details.

    white label website builder

    ReplyDelete
  3. Nice and informative article.Thanks for sharing such nice article, keep on updating.

    Python Training in Chennai
    Python Training

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. I really like the dear information you offer in your articles. I’m able to bookmark your site and show the kids check out up here generally. Im fairly positive theyre likely to be informed a great deal of new stuff here than anyone
    Click here:
    Microsoft azure training in annanagar
    Click here:
    Microsoft azure training in velarchery

    ReplyDelete
  6. I found your blog while searching for the updates, I am happy to be here. Very useful content and also easily understandable providing.. Believe me I did wrote an post about tutorials for beginners with reference of your blog. 
    Click here:
    angularjs Training in online
    Click here:
    angularjs training in annanagar
    Click here:
    angularjs training in bangalore
    Click here:
    angularjs training in chennai

    ReplyDelete
  7. This is a nice article here with some useful tips for those who are not used-to comment that frequently. Thanks for this helpful information I agree with all points you have given to us. I will follow all of them.
    Blueprism training in Chennai

    Blueprism training in Bangalore

    Blueprism training in Pune

    Blueprism online training

    Blueprism training in tambaram

    ReplyDelete
  8. Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.
    Devops training in velachery
    Devops training in annanagar
    Devops training in sholinganallur

    ReplyDelete
  9. Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.
    Devops training in velachery
    Devops training in annanagar
    Devops training in sholinganallur

    ReplyDelete
  10. I have been meaning to write something like this on my website and you have given me an idea. Cheers.

    java training in marathahalli | java training in btm layout

    ReplyDelete
  11. We are a group of volunteers and starting a new initiative in a community. Your blog provided us valuable information to work on.You have done a marvellous job!
    Data Science course in rajaji nagar | Data Science with Python course in chenni
    Data Science course in electronic city | Data Science course in USA
    Data science course in pune | Data science course in kalyan nagar

    ReplyDelete
  12. Hmm, it seems like your site ate my first comment (it was extremely long) so I guess I’ll just sum it up what I had written and say, I’m thoroughly enjoying your blog. I as well as an aspiring blog writer, but I’m still new to the whole thing. Do you have any recommendations for newbie blog writers? I’d appreciate it.

    AWS Interview Questions And Answers

    AWS Training in Pune | Best Amazon Web Services Training in Pune

    Amazon Web Services Training in Pune | Best AWS Training in Pune

    AWS Online Training | Online AWS Certification Course - Gangboard

    ReplyDelete
  13. Goyal packers and movers in Panchkula is highly known for their professional and genuine packing and moving services. We are top leading and certified relocation services providers in Chandigarh deals all over India. To get more information, call us.


    Packers and movers in Chandigarh
    Packers and movers in Panchkula
    Packers and movers in Mohali
    Packers and movers in Zirakpur
    Packers and movers in Patiala
    Packers and movers in Ambala
    Packers and movers in Ambala cantt
    Packers and movers in Pathankot
    Packers and movers in Jalandhar
    Packers and movers in Ludhiana

    ReplyDelete
  14. Thank you so much for your information,its very useful and helful to me.Keep updating and sharing. Thank you.
    RPA training in chennai | UiPath training in chennai


    ReplyDelete
  15. I think things like this are really interesting. I absolutely love to find unique places like this. It really looks super creepy though!!

    machine learning training in Chennai

    machine learning training center in chennai

    top institutes for machine learning in chennai

    ReplyDelete
  16. Such a Great Article!! I learned something new from your blog. Amazing stuff. I would like to follow your blog frequently. Keep Rocking!!
    Blue Prism training in chennai | Best Blue Prism Training Institute in Chennai

    ReplyDelete
  17. Thanks for such a great article here. I was searching for something like this for quite a long time and at last, I’ve found it on your blog. It was definitely interesting for me to read about their market situation nowadays.AngularJS Training in Chennai | Best AngularJS Training Institute in Chennai

    ReplyDelete
  18. Given so much info in it, The list of your blogs are very helpful for those who want to learn more interesting facts. Keeps the users interest in the website, and keep on sharing more
    Our Credo Systemz Which is designed to offer you OpenStack Training skills required to kick-start your journey as an OpenStack Cloud Administrator.
    Please free to call us @ +91 9884412301 / 9600112302
    Openstack course training in Chennai | best Openstack course in Chennai | best Openstack certification training in Chennai | Openstack certification course in Chennai | openstack training in chennai omr | openstack training in chennai velachery | openstack training in Chennai | openstack course fees in Chennai | openstack certification training in Chennai | best openstack training in Chennai | openstack certification in Chennai

    ReplyDelete
  19. Wow!! Really a nice Article. Thank you so much for your efforts. Definitely, it will be helpful for others. I would like to follow your blog. Share more like this. Thanks Again.
    iot training in Chennai | Best iot Training Institute in Chennai

    ReplyDelete
  20. Played on BGAOC with big wins? NOT? Come to us as soon as possible and win with us. slot machines that Come get your chance to win.

    ReplyDelete
  21. Great post! I am actually getting ready to across this information, It's very helpful for this blog. Also great with all of the valuable information you have Keep up the good work you are doing well.DevOps Training in Chennai | Best DevOps Training Institute in Chennai

    ReplyDelete
  22. Wow!! Really a nice Article. Thank you so much for your efforts. Definitely, it will be helpful for others. I would like to follow your blog. Share more like this. Thanks Again.
    React js training in Chennai | Best React js training institute in Chennai | Best React js training near me | React js training online

    ReplyDelete
  23. I am really enjoying reading your well written articles.
    It looks like you spend a lot of effort and time on your blog.
    I have bookmarked it and I am looking forward to reading new articles. Keep up the good work..
    Cloud Computing Courses in Chennai
    Cloud Computing Training in Chennai
    AWS Training in Chennai
    Data Science Course in Chennai
    Digital Marketing Course in Chennai
    Cloud Computing Training in Anna Nagar
    Cloud Computing Courses in Adyar

    ReplyDelete
  24. Are you trying to move in or out of Jind? or near rohtak Find the most famous, reputed and the very best of all Packers and Movers by simply calling or talking to Airavat Movers and Packers

    Packers And Movers in Jind

    Packers And Movers in Rohtak

    Movers And Packers in Rohtak

    ReplyDelete
  25. Alot of blogs I see these days don't really provide anything that I'm interested in, but I'm most definitely interested in this one. Just thought that I would post and let you know. Nice! thank you so much! Thank you for sharing.

    ReplyDelete
  26. Эксклюзивная лента светодиодная для подсветки дизайнерского освещения и уникальных светильников я обычно беру у Ekodio

    ReplyDelete
  27. Thanks for such a great article here. I was searching for something like this for quite a long time and at last, I’ve found it on your blog. It was definitely interesting for me to read about their market situation nowadays.angularjs best training center in chennai | angularjs training in velachery | angularjs training in chennai | angularjs training in omr

    ReplyDelete
  28. Good Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.
    lenovo service center in velachery
    lenovo service center in porur
    lenovo service center in vadapalani

    ReplyDelete
  29. Thanks for your post which gather more knowledge about this topic. I read your blog everytime which is helpful and effective.
    Digital marketing training in chennai
    Digital marketing course in chennai
    SEO training in chennai

    ReplyDelete
  30. All the points you described so beautiful. Every time i read your i blog and i am so surprised that how you can write so well.
    angularjs online training

    apache spark online training

    informatica mdm online training

    devops online training

    aws online training

    ReplyDelete
  31. Nice post. Thanks for sharing! I want people to know just how good this information is in your article. It’s interesting content and Great work.
    Thanks & Regards,
    VRIT Professionals,
    No.1 Leading Web Designing Training Institute In Chennai.

    And also those who are looking for
    Web Designing Training Institute in Chennai
    SEO Training Institute in Chennai
    Photoshop Training Institute in Chennai
    PHP & Mysql Training Institute in Chennai
    Android Training Institute in Chennai

    ReplyDelete
  32. The post is written in very a good manner and it entails many useful information for me. I am happy to find your distinguished way of writing the post. Now you make it easy for me to understand and implement the concept.

    ReactJS Online Training

    ReplyDelete
  33. This is an awesome post. Really very informative and creative contents.
    ios app Devlopment company in chennai

    ReplyDelete
  34. Thanks for sharing an informative blog keep rocking bring more details.I like the helpful info you provide in your articles. I’ll bookmark your weblog and check again here regularly. I am quite sure I will learn much new stuff right here! Good luck for the next!
    web designing classes in chennai | web designing training institute in chennai
    web designing and development course in chennai | web designing courses in Chennai
    best institute for web designing in chennai | web designing course with placement in chennai

    ReplyDelete
  35. लिंग 2-3 इंच लम्बा, 1 इंच मोटा करें यौन शक्ति को बढ़ायें, वीर्य को ताकतवर बनायें तथा, शीघ्रपतन की समस्या को जड़ से समाप्त करें। ODER NOW :- http://www.ubracare.com/hot/

    ReplyDelete