Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Synchronizing remote JSON data to local cache storage in iOS Swift

I am trying to find the solution for simple processing all necessary steps for read-only consuming remote JSON data on iOS devices. It means fetching remote JSON data, store to local cache on iOS device for offline usage, refresh the cache, parsing JSON data. I think it is very common requirement for all mobile apps nowadays.

I know it is possible to manually download remote JSON file, store it to local db or file on iOS device and when network is not available fetch it from local storage otherwise dowload it from net. I do it manually now. :) But it is lot of steps which hope is possible to do by using frameworks/libraries, isn't?

So I tried HanekeSwift framework which do almost everything what I need but it only do caching remote JSON (and Images) but doesn't refresh the cache!! which is not useful for me. I know also that exists Alamofire and SwiftyJSON but I don't have any experience with them.

Do you have any experience how to do that?

Summary

  • libraries or frameworks for iOS8 support in Swift
  • download remote JSON and store to local cache
  • possibility to refresh local cache from it's origin
  • nice bonus is easy JSON parsing

enter image description here

like image 488
kolisko Avatar asked Feb 09 '15 19:02

kolisko


3 Answers

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.

like image 116
cnoon Avatar answered Nov 07 '22 05:11

cnoon


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()
            }
    }
}
like image 3
Charz Avatar answered Nov 07 '22 04:11

Charz


This is a Swift 3 version based on Charl's answer (using SwiftyJSON and Alamofire):

func getData(){

    let query_url = "http://YOUR-URL-HERE"   

    // escape your URL
    let urlAddressEscaped = query_url.addingPercentEncoding(withAllowedCharacters:NSCharacterSet.urlQueryAllowed)


    //Request with caching policy
    let request = URLRequest(url: URL(string: urlAddressEscaped!)!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 20)

    Alamofire.request(request)
        .responseJSON { (response) in
            let cachedURLResponse = CachedURLResponse(response: response.response!, data: (response.data! as NSData) as Data, userInfo: nil, storagePolicy: .allowed)
            URLCache.shared.storeCachedResponse(cachedURLResponse, for: response.request!)

            guard response.result.error == nil else {

                // got an error in getting the data, need to handle it
                print("error fetching data from url")
                print(response.result.error!)
                return

            }

            let json = JSON(data: cachedURLResponse.data) // SwiftyJSON

            print(json) // Test if it works

            // do whatever you want with your data here

    }
}
like image 2
lenooh Avatar answered Nov 07 '22 05:11

lenooh