Update November 2015: Fully updated for iOS 9 (Xcode 7 and Swift 2).
Almost all applications require a mechanism to store data that will be needed at a later time. For example, you will need a way to store player scores for your game, some static data for tables, or words and sentences for your app localisation. For the previously mentioned cases, property lists (aka plist) are the convenient way to store, organise and access small amounts of data.
In this quick tutorial, I gonna show you how easy it is to deal with plists, commonly how to read data from plist and how to write and store data back to it.
To start off, download the starter project here.
Open up the project and take a look at the main files in there.
I have prepared some files like “notes.plist” that is storing the current data, I filled it with some dummy data for now before you will add and edit its content programmatically.
Also, the file “NewNoteViewController.swift” will be responsible of saving a new note to the plist data file. For the screens, take a look at the “Main.storyboard” file to see how all UI are set up together.
If you run the project, you will get the main idea of the app. The first screen will list the content stored in the “notes.plist” file while the “Edit” button allows to remove some rows from the app model, and the plus button at the top right will load another screen where you can add some notes to the plist file.
Now we need to implement some code to make all this happen π
Select the “AppDelegate.swift” file from the Project navigator view and add the following variable declaration below the class name.
var plistPathInDocument:String = String()
The above variable will store the path for the plist file and will be accessible all over the app.
Next, place the following method implementation code below the variable declaration.
func preparePlistForUse(){ // 1 let rootPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, .UserDomainMask, true)[0] // 2 plistPathInDocument = rootPath.stringByAppendingString("/notes.plist") if !NSFileManager.defaultManager().fileExistsAtPath(plistPathInDocument){ let plistPathInBundle = NSBundle.mainBundle().pathForResource("notes", ofType: "plist") as String! // 3 do { try NSFileManager.defaultManager().copyItemAtPath(plistPathInBundle, toPath: plistPathInDocument) }catch{ print("Error occurred while copying file to document \(error)") } } }
Let’s break down the code above:
// 1: NSSearchPathDirectory specifies the location of a variety of directories in your device, here you just asked for the document directory path.
// 2: Here you just create a new path in the document directory for the notes.plist file, the reason why you are creating a path in the document directory is because the already created notes.plist file is currently located in the main bundle of the app which means that you don’t have write permissions on it, and hence you can not edit its content, so we will need to move the plist file from the main bundle to another directory (like the document directory) to be able to edit its content.
// 3: The if block will test whether the path you created in step 2 for the plist file already exists in the document directory, if not, it will copy the notes.plist file from the main bundle to the document directory as I explained previously. The copyItemAtPath API you used can throw errors, so it’s important to use it carefully with the do-catch block as you did.
Note: The key point is that you need to move the file out from the main bundle to be able to edit it, this is what the previously implemented method does.
Now let’s make sure the plist file is always prepared for us to read and write. This can be done by invoking the “preparePlistForUse” method above from within the didFinishLaunchingWithOptions and applicationDidBecomeActive application delegate methods.
To do so, place the following call inside the didFinishLaunchingWithOptions method (just before the return statement) and inside the applicationDidBecomeActive method:
self.preparePlistForUse()
Good, you just implemented the tricky part of the project, now that the plist file is placed inside the document directory, we can work on it comfortably!
Select ViewController.swift file from the Project navigator view and place the following variables declaration right after the class name:
var notesArray:NSMutableArray! var plistPath:String!
Next, implement the “viewWillAppear” method inside the class, like below:
override func viewWillAppear(animated: Bool) { let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate plistPath = appDelegate.plistPathInDocument // Extract the content of the file as NSData let data:NSData = NSFileManager.defaultManager().contentsAtPath(plistPath)! do{ notesArray = try NSPropertyListSerialization.propertyListWithData(data, options: NSPropertyListMutabilityOptions.MutableContainersAndLeaves, format: nil) as! NSMutableArray }catch{ print("Error occured while reading from the plist file") } self.tableView.reloadData() }
As you may notice, the method above will firstly get the path of the plist file inside the document directory, then it will extract its content and assign it to the notesArray variable which is the data source of the table view, then it will reload the table view to populate the data. As the propertyListWithData API may throw errors, you invoked it inside a do-catch block to handle any potential errors.
Note: You may wonder why you implemented the code above inside the viewWillAppear method instead of the viewDidLoad. This is because you need a way to be notified each time the view controller is shown, especially when you add a new note and the presented controller gets dismissed. You will see this in a bit π
Now, let’s implement the table view data source protocol methods in order to populate the data. Place the following code before the closing bracket of the class:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{ let cell:UITableViewCell! = tableView.dequeueReusableCellWithIdentifier("cellIdentifier") cell.textLabel!.text = notesArray.objectAtIndex(indexPath.row) as? String return cell } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{ return notesArray.count }
So far so good, run the app to test, the data should be loaded correctly inside the table view.
Next, let’s implement the “Edit” button action. Remember, the Edit button will toggle the editing mode of the table view so that you can delete rows directly from the view and the plist file.
Switch to the Assistant editor, make sure Main.storyboard is loaded along with the ViewController.swift file.
Ctrl+drag from the Edit button in the canvas to the class file, make sure it’s an Action connection, name it “editTable” and click the connect button.
Now, place the following code inside the newly created “editTable” action method:
if self.tableView.editing{ let leftBarButtonItem:UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Edit, target: self, action: "editTable:") self.navigationItem.leftBarButtonItem = leftBarButtonItem self.tableView.setEditing(false, animated: true) }else{ let leftBarButtonItem:UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self, action: "editTable:") self.navigationItem.leftBarButtonItem = leftBarButtonItem self.tableView.setEditing(true, animated: true) }
Cool, run the app and click the Edit button, now the table view is switching between the editing and the normal (selection) modes.
In order to be able to delete rows from the table view, we need to implement one more protocol method, so that the deletion will be committed by the data source. Place the following method before the closing bracket of the ViewController.swift file.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath){ // remove the row from the array notesArray.removeObjectAtIndex(indexPath.row) self.tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade) notesArray.writeToFile(plistPath, atomically: true) }
Here we go, when you press the Delete red button, the data source will be notified in the commitEditingStyle method above. At that moment, you firstly removed the row from the table view data source array, then you ask the table view to delete it from the visual rows in the screen (with a fade animation). And as a last step (and the most important one), you committed the updated array and write it back to the plist file so that you get always an up to date version of your data π
Run the app and try to delete some rows to test everything is working as expected.
We are almost done, let’s quickly add some code to add a new notes to the plist file. Select the Assistant editor and make sure that “NewNoteViewController.swift” and “Main.storyboard” files are loaded side by side in the editor.
Follow the steps below to hook up some objects and action triggers to the code:
1- Ctrl+drag from the text view object to the code, make sure it’s an Outlet connection and name it “textView”.
2- Ctrl+drag from the “Cancel” button to the code, this time make sure it’s an Action connection and name it “cancel”.
3- Ctrl+drag from the “Save” button to the code, it should be an Action connection as well. Name it “saveNote”.
Here is the added code in NewNoteViewController.swift file:
@IBOutlet var textView: UITextView! @IBAction func cancel(sender: AnyObject) { } @IBAction func saveNote(sender: AnyObject) { }
Go ahead and place the following implementation inside the cancel action method to dismiss the view controller:
self.dismissViewControllerAnimated(true, completion: nil)
Next, implement the following code inside the “saveNote” action method:
// Save note to plist let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate let pathForThePlistFile = appDelegate.plistPathInDocument // Extract the content of the file as NSData let data:NSData = NSFileManager.defaultManager().contentsAtPath(pathForThePlistFile)! // Convert the NSData to mutable array do{ let notesArray = try NSPropertyListSerialization.propertyListWithData(data, options: NSPropertyListMutabilityOptions.MutableContainersAndLeaves, format: nil) as! NSMutableArray notesArray.addObject(self.textView.text) // Save to plist notesArray.writeToFile(pathForThePlistFile, atomically: true) }catch{ print("An error occurred while writing to plist") } // Dismiss the modal controller self.dismissViewControllerAnimated(true, completion: nil)
Basically, the code above will almost do the same work as the delete operation you implemented previously in this tutorial, except that this time, you gonna add content to the plist file instead of removing data.
Finally, let’s add one extra line of code to make the keyboard show up automatically once the view controller is loaded. Add the following statement to the viewDidLoad method, just below the super.viewDidLoad() call:
self.textView.becomeFirstResponder()
That’s it! run the app, try to add some notes and save them to the plist. Play around with the project and enjoy your work π
As usual, you can download the final project here.
Let’s summarise:
– If you want to read from a plist file, it’s ok to extract its path from the main bundle. However, if you want to write on it, you need to move it from the bundle to another location, like the document directory as you did in this tutorial.
– Remember, each time you add, edit, or remove content from the plist file, you need to write your data back to the file using the “writeToFile” API.
Ok folks, that’s it for this tutorial. Don’t hesitate to join the discussion below and leave a comment, I would love to hear your thoughts!
Hello.
Im doing everything as you oriented for this project by Malek but I’m having a few errors when copying the “func preparePlistForUse()”. This is one of the first steps to complete the build.
Please help me find out whats wrong with the code. I’m attaching a print screen of what I’m getting.
Thanks for the help!
Hi, you have to catch something in this example you want to catch some error like that :
} catch let error as NSError {
Hi Ramses, which Xcode version you are working with? please make sure you are using Xcode 7 as the error handling are introduced with Swift 2.
Hello.
I’m trying to use it on my app but I’m getting this error:
Could not cast value of type ‘__NSCFDictionary’ (0x4e145c) to ‘NSMutableArray’ (0x4e11c8)
On this line: notesArray = try NSPropertyListSerialization.propertyListWithData(data, options: NSPropertyListMutabilityOptions.MutableContainersAndLeaves, format: nil) as! NSMutableArray
Could you help me?
Hi Luis π
Most likely the Root of your PLIST file is a Dictionary and not an Array. Changing the Root type to “Array” will surely fix the error.
Hi Malek_T thanks for the answer! I had already checked that but I didn’t remove the app from the simulator, just had restart it. Now it works! So much trouble for that silly thing, at least we can learn from our mistakes ahah
Thank you so much!
Malek_T I just have one problem in my app right now…
I’m not using a navigation controller, I insert your code just in a TableViewController. The problem is that I needed a button to go from the TableViewController to other ViewController but I can’t make it happen. Could you help me?
Sure Luis π
Please move your question to the forum section with as much details as you can, I will be glad to assist you there!
I’m having two issues. 1. New notes I create are not saving to the Notes list. 2. I’m getting this error message when I click on Edit the second time: “libc++abi.dylib: terminating with uncaught exception of type NSException” in the console every once in a while”.
I was missing a few lines of code for issue #1. I see notes saved to the plist now but only after I reload the app.
I corrected issue #2. It is because “editTable:” has been deprecated in Swift 2.2 and the suggested fix from Swift doesn’t work properly (rarely ever does).
https://uploads.disquscdn.com/images/2aef9f7869bd9de813962654f3ccb2b8d9098ab99450afa478a5482d1600deae.png
Hi Malek, I followed your article and this was very very helpful to save data into Plist.
What I did is; I have some data set called “DefData.plist” and it’s root is “Dictionary”, and I have stored some default data set such as Arrays and Dictionaries inside that file.
Also I have another Plist called “List.plist” and it’s Root is Array. I’m going to write data into “List.plist as below.
1. Read the DefData.plist and stored into a var defData = NSDictionary()
2. Get the alertView text for key: “name”, value: self.textField.text and key: “data”, value: self.defData
var dictData:[String:AnyObject] = [String:AnyObject]()
3. Then I stored those 2 values as a NSDictionary into List.plist. It was successful. I checked the the Plist and data have been stored correctly. Also that’s what I needed.
4. Then I bring those values to next view controllers such as detailViewController. But the problem is how can I edit the values.
(i) I need to edit name and save new name
(ii) Also I need to edit that defData.plist as well. (Inside saved Plist => Array Item => Saved Dictionary => Edit “name” String as well as “data” Dictionary.
5. Could you please help me to implement that ? Because I just needed to store defData as default data when saving a new row. But now I need to edit that defData dictionary and save new values as user inputs (Update Plist)
I hope your kind help..
Thank you in advance..
Dushan
This became a lot easier with Swift 4 as per WWDC 2017 session 212.
Any plans on updating this post foi iOS11?