Synchronizing remote JSON data to local cache storage in iOS Swift

Solution 1:

Great question!

You can absolutely accomplish this with a combination of Alamofire and SwiftyJSON. What I would recommend is a combination of several things to make this as easy as possible.

I think you have two approaches to fetching the JSON.

  1. Fetch the JSON data in-memory and use a cache policy
  2. Download the JSON data to disk directly to your local cache

Option 1

// Create a shared URL cache
let memoryCapacity = 500 * 1024 * 1024; // 500 MB
let diskCapacity = 500 * 1024 * 1024; // 500 MB
let cache = NSURLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "shared_cache")

// Create a custom configuration
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
var defaultHeaders = Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders
configuration.HTTPAdditionalHeaders = defaultHeaders
configuration.requestCachePolicy = .UseProtocolCachePolicy // this is the default
configuration.URLCache = cache

// Create your own manager instance that uses your custom configuration
let manager = Alamofire.Manager(configuration: configuration)

// Make your request with your custom manager that is caching your requests by default
manager.request(.GET, "http://httpbin.org/get", parameters: ["foo": "bar"], encoding: .URL)
       .response { (request, response, data, error) in
           println(request)
           println(response)
           println(error)

           // Now parse the data using SwiftyJSON
           // This will come from your custom cache if it is not expired,
           // and from the network if it is expired
       }

Option 2

let URL = NSURL(string: "/whereever/your/local/cache/lives")!

let downloadRequest = Alamofire.download(.GET, "http://httpbin.org/get") { (_, _) -> NSURL in
    return URL
}

downloadRequest.response { request, response, _, error in
    // Read data into memory from local cache file now in URL
}

Option 1 certainly leverages the largest amount of Apple supported caching. I think with what you're trying to do, you should be able to leverage the NSURLSessionConfiguration and a particular cache policy to accomplish what you're looking to do.

Option 2 will require a much larger amount of work, and will be a bit of a strange system if you leverage a cache policy that actually caches data on disk. Downloads would end up copying already cached data. Here's what the flow would be like if your request existed in your custom url cache.

  1. Make download request
  2. Request is cached so cached data loaded into NSInputStream
  3. Data is written to the provided URL through NSOutputStream
  4. Response serializer is called where you load the data back into memory
  5. Data is then parsed using SwiftyJSON into model objects

This is quite wasteful unless you are downloading very large files. You could potentially run into memory issues if you load all the request data into memory.

Copying the cached data to the provided URL will most likely be implemented through NSInputStream and NSOutputStream. This is all handled internally by Apple by the Foundation framework. This should be a very memory efficient way to move the data. The downside is that you need to copy the entire dataset before you can access it.

NSURLCache

One other thing that may be very useful here for you is the ability to fetch a cached response directly from your NSURLCache. Take a look at the cachedReponseForRequest: method which can be found here.

SwiftyJSON

The last step is parsing the JSON data into model objects. SwiftyJSON makes this very easy. If you're using Option 1 from above, then you could use the custom response serializer in the Alamofire-SwiftyJSON. That would look something like the following:

Alamofire.request(.GET, "http://httpbin.org/get", parameters: ["foo": "bar"])
         .responseSwiftyJSON { (request, response, json, error) in
             println(json)
             println(error)
         }

Now if you used Option 2, you'll need to load the data from disk, then initialize a SwiftyJSON object and begin parsing which would look something like this:

let data = NSData(contentsOfFile: URL.path)!
let json = JSON(data: data)

That should cover all the tools in that you should need to accomplish what you attempting. How you architect the exact solution is certainly up to you since there are so many possible ways to do it.

Solution 2:

Below is the code i used to cache my requests using Alamofire and SwiftyJSON - I hope it helps someone out there

func getPlaces(){
    //Request with caching policy
    let request = NSMutableURLRequest(URL: NSURL(string: baseUrl + "/places")!, cachePolicy: .ReturnCacheDataElseLoad, timeoutInterval: 20)
    Alamofire.request(request)
        .responseJSON { (response) in
            let cachedURLResponse = NSCachedURLResponse(response: response.response!, data: (response.data! as NSData), userInfo: nil, storagePolicy: .Allowed)
            NSURLCache.sharedURLCache().storeCachedResponse(cachedURLResponse, forRequest: response.request!)

            guard response.result.error == nil else {
                // got an error in getting the data, need to handle it
                print("error calling GET on /places")
                print(response.result.error!)
                return
            }

            let swiftyJsonVar = JSON(data: cachedURLResponse.data)
            if let resData = swiftyJsonVar["places"].arrayObject  {
                // handle the results as JSON, without a bunch of nested if loops
                self.places = resData

                //print(self.places)

            }
            if self.places.count > 0 {
                self.tableView.reloadData()
            }
    }
}