62

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

mfaani
  • 33,269
  • 19
  • 164
  • 293
kolisko
  • 1,548
  • 3
  • 17
  • 22

3 Answers3

39

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.

cnoon
  • 16,575
  • 7
  • 58
  • 66
  • Thanks for answer! Only possible solution for me is Option 2 to store data on local disk. What exactly Alamofire does when 1. network connection is not available and 2. when is? ad1 - Load data from cache if is available, ad2 - refresh data in cache? Maybe stupid question but is it right? – kolisko Feb 11 '15 at 19:34
  • If a network connection is not available, then you will receive a network error in the `response`. You could certainly add logic to only attempt to download data if you have an internet connection. As for your other questions, I have made some large updates to the answer above around `NSURLCache` and different `NSURLRequestCachePolicy`s. Hopefully that gives you better direction around your options for leveraging Apple's caching system that already exists. – cnoon Feb 12 '15 at 06:16
  • 2
    In my app, I would use the Option 1 and use the `NSURLCache`. I would like to use the cached response when I have no connection available. Does it exist a way to use the cache directly, without make the request, in the case of connection absence? Thanks – giograno May 04 '16 at 14:45
  • What happens if I use both things at a time? – Sivajee Battina Oct 01 '18 at 07:38
  • @cnoon Can you please explain me How much will the Expiry time of cash data? and it will automatically update during the service call? – Nikunj Kumbhani Oct 05 '18 at 06:52
  • Sorry to bother Christian. Can you take a look at [URLResponse is not retrieved after storing in cache using storeCachedResponse Ask](https://stackoverflow.com/questions/52938033/urlresponse-is-not-retrieved-after-storing-in-cache-using-storecachedresponse) – mfaani Oct 23 '18 at 15:23
3

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()
            }
    }
}
Charz
  • 41
  • 4
2

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

    }
}
lenooh
  • 10,364
  • 5
  • 58
  • 49
  • By calling dynamic(different request url and parameter) request, I'm getting same response, which is stored in cache at the first time. Any idea ? – Jayaraj Mar 04 '17 at 09:24
  • @lenooh. is it possible to store data offline using Alamofire in swift 3 – Uma Madhavi May 15 '17 at 06:44
  • @jayaraj & lenooh .. This is my code ,https://ibb.co/nKKXkk. How can i achieve this offline storage in swift 3 – Uma Madhavi May 15 '17 at 07:32