Offline support without Core Data with NSCoding in iOS
Recently I received a lot of comments about how to persist data locally on a device to support offline mode.
The scenario is the following: you have an app that downloads content from a server, user downloads it, everything is fine. But what if they close the app, lost network and open it again to see their saved favorites. If you don’t support offline mode they will not be able to see anything or interact with their content.
There are multiple ways to solve this issue:
- Use a database like Core Data and sync/save everything into it, so when you’re connection drops you can just load the data from the database.
- Save your downloaded content directly into files with the help of NSCoding. You are mainly going to download data in a .json format, so why don’t you save the whole file to disk and load it when necessary?
To learn more about NSCoding make sure to check out the official documentation from Apple here.
I’m going to show you how to do the last option. But we go easy on this one for now. Let’s assume you have a Recipe app, and you wish to add a possibility to save your recipes as Favorites on your device locally.
What is NSCoding?
NSCoding is a protocol that you need to implement on your data class if you wish to support the encoding and decoding of that object. You must implement two methods of NSCoding that allows you to specify which data to persist.
1 2 3 4 5 6 7 8 9 10 |
class Recipe: NSObject, NSCoding { required convenience init?(coder aDecoder: NSCoder) { // Required } func encodeWithCoder(aCoder: NSCoder) { // Required } } |
As you can see, this is all you need to setup your code to be able to archive and unarchive it. But let’s see how it is done with real objects and properties in place.
Cache everything with NSCoding
Our Recipe object looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/// Object that holds all the properties of a Recipe class Recipe: NSObject, NSCoding { /// Path of the corresponding json file var jsonFilePath: String? /// Path of the main image var mainImagePath: String? /// Title var title: String = "" /// Difficulty level of the Recipe var difficulty: String = "" /// Preparation time var preparationTime: String = "" /// How many portions it serves var portions: String = "" /// Description of the Recipe var descriptionText: String = "" /// Ingredients it needs var ingredients: [Ingredient] = [Ingredient]() /// Steps of the Recipe var cookingSteps: [CookingStep] = [CookingStep]() |
These are all the properties we are going to save when a user sets it as a favorite. If you have nested objects like we do (CookingStep, Ingredient classes), you need to implement the NSCoding protocol there too. Therefore, the whole object with all its objects will be saved on disk.
First of all let’s look at the encoding part. NSCoding protocol contains a method called: encodeWithCoder(aDecoder: NSCoder). We use the aDecoder variable to encode all our properties. Note that it has designated encoder methods for Int, Float, object, boolean etc.
1 2 3 4 5 6 7 8 9 10 |
func encodeWithCoder(aCoder: NSCoder) { aCoder.encodeObject(title, forKey: PropertyKey.titleKey) aCoder.encodeObject(mainImagePath, forKey: PropertyKey.mainImagePathKey) aCoder.encodeObject(difficulty, forKey: PropertyKey.difficultyKey) aCoder.encodeObject(preparationTime, forKey: PropertyKey.prepTimeKey) aCoder.encodeObject(portions, forKey: PropertyKey.portionsKey) aCoder.encodeObject(descriptionText, forKey: PropertyKey.descriptionKey) aCoder.encodeObject(ingredients, forKey: PropertyKey.ingredientsKey) aCoder.encodeObject(cookingSteps, forKey: PropertyKey.cookingStepsKey) } |
You basically take all your properties and call aCoder.encodeObject(TheObject, forKey: KeyForYourObject). The encode method expects two parameters, one is the object that you wish to encode, and the key you wish to assign it too. Same like as you would do it in a dictionary (define a value for a key). If you have nested objects – like I do – you need to do the same thing on those objects too.
That’s all for encoding.
Decode your objects
Now that you encoded your objects properly, time to decode them when you wish to read them from a file. You’ll need an init method that you implemented earlier (see above) and another init method (optional) that uses all your decoded data as parameters.
In our example it will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
required convenience init?(coder aDecoder: NSCoder) { let title = aDecoder.decodeObjectForKey(PropertyKey.titleKey) as! String let mainImagePath = aDecoder.decodeObjectForKey(PropertyKey.mainImagePathKey) as! String let difficulty = aDecoder.decodeObjectForKey(PropertyKey.difficultyKey) as! String let prepTime = aDecoder.decodeObjectForKey(PropertyKey.prepTimeKey) as! String let portions = aDecoder.decodeObjectForKey(PropertyKey.portionsKey) as! String let descriptionText = aDecoder.decodeObjectForKey(PropertyKey.difficultyKey) as! String let ingredients = aDecoder.decodeObjectForKey(PropertyKey.ingredientsKey) as! [Ingredient] let steps = aDecoder.decodeObjectForKey(PropertyKey.cookingStepsKey) as! [CookingStep] self.init(mainImagePath: mainImagePath, title: title, difficulty: difficulty, preparationTime: prepTime, portions: portions, descriptionText: descriptionText, ingredients: ingredients, steps: steps)! } init?(mainImagePath: String, title: String, difficulty: String, preparationTime: String, portions: String, descriptionText: String, ingredients: [Ingredient], steps: [CookingStep]) { self.title = title self.mainImagePath = mainImagePath self.difficulty = difficulty self.preparationTime = preparationTime self.portions = portions self.descriptionText = descriptionText self.ingredients = ingredients self.cookingSteps = steps super.init() } |
I hope it makes sense to you but if it didn’t, let me explain it to you.
You opened the app and want to load your favorite Recipes therefore you call your manager object to load all objects. First of all, it grabs all the data it found and call the init?(coder…) method. Than you define which objects you want to get for a given key and assign it to a variable, than call the designated init method with all your properties and move on to the next object in your favorites. Just simply call: aDecoder.decodeObjectForKey(YourKeyForYourObject) and specify what type of object you wish to get.
Manage saving and loading objects
Now that you have NSCoding in place, let’s look at how to do save and load these objects.
Saving
We’ll have a manager object to help us organize our code and make our life easier. Therefore let’s create a new class called: FavoritesManager a subclass of NSObject.
It will have 2 methods:
- saveRecipes(recipes: [Recipe])
- saves an array of recipes
- readFavoritesFromDisk() -> [Recipe]?
- loads all the saved Recipe objects from disk
1.
1 2 3 4 5 6 |
func saveFavoritesToDisk(recipes: [Recipe]) { let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(recipes, toFile: FavoritesManager.ArchiveURL.path!) if !isSuccessfulSave { print("Failed to save recipes...") } } |
2.
1 2 3 4 5 |
func readFavoritesFromDisk() { if let results = NSKeyedUnarchiver.unarchiveObjectWithFile(FavoritesManager.ArchiveURL.path!) as? [Recipe] { favoriteRecipesArray = results } } |
As you might noticed I skipped an important part which is the where… It’s okay to save and load your saved recipes but how do you tell your app where it can find them.
You can just add 2 new helpers:
1 2 3 4 |
/// Reference to the Documents folder static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first! /// Reference to the Favorites folder where the favorited Recipes will be held static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("Favorites") |
That will serve as a reference to a folder, where the manager object should save your recipes to and load them from.
Wrap up
So it is really that simple. For years I had no idea what NSCoding is and how to use it, because of that I just skipped it and went the long way, boy I was wrong… Since everything in iOS or life is hard you tend to just pass it but cannot avoid it at the end.
Hope it was helpful to you and learned something today about NSCoding and lightweight caching.
I have a simple template on Codecanyon.net that uses this technique, if you wish to check it out, click on the button below.
Have an awesome coding day!