Reading and writing Property List files with Codable in Swift Published by on

You have probably seen and used a property list file at some point in your iOS journey. I know you have because every iOS app has an Info.plist file. It's possible to create and store your own .plist files to hold on to certain data, like user preferences that you don't want to store in UserDefaults for any reason at all. In this week's Quick Tip you will learn how you can read and write data from and to property list files using Swift's Codable protocol.

Defining a model that can be stored in a property list

Because Swift has special PropertyListEncoder and PropertyListDecoder objects, it's possible to define the model that you want to store in a property list using Codable :

struct APIPreferences: Codable { var apiKey: String var baseURL: String }

This model is trivial but you can create far more complex models if you want. Any model that conforms to Codable can be used with property lists. If you haven't worked with Codable before, check out this post from my Antoine van der Lee to get yourself up to speed. His post is about JSON parsing, but everything he writes about defining models applies to property lists as well.

Loading a model from a property list

We can load plist files from the filesystem using the FileManager object. Let's dive right in with some code; this is a Quick Tip after all.

class APIPreferencesLoader { static private var plistURL: URL { let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! return documents.appendingPathComponent("api_preferences.plist") } static func load() -> APIPreferences { let decoder = PropertyListDecoder() guard let data = try? Data.init(contentsOf: plistURL), let preferences = try? decoder.decode(APIPreferences.self, from: data) else { return APIPreferences(apiKey: "", baseURL: "") } return preferences } }

I defined a simple class here because this allows me to use the APIPreferenceLoader in a brief example at the end of this post.

The plistURL describes the location of the property list file on the file system. Since it's a file that we want to create and manage at runtime, it needs to be stored in the documents directory. We could store an initial version of the plist file in the bundle but we'd always have to copy it over to the documents directory to update it later because the bundle is read-only. You might use the following code to perform this copy step:

extension APIPreferencesLoader { static func copyPreferencesFromBundle() { if let path = Bundle.main.path(forResource: "api_preferences", ofType: "plist"), let data = FileManager.default.contents(atPath: path), FileManager.default.fileExists(atPath: plistURL.path) == false { FileManager.default.createFile(atPath: plistURL.path, contents: data, attributes: nil) } } }

This code extracts the default preferences from the bundle and checks whether a stored property list exists in the documents directory. If no file exists in the documents directory, the data that was extracted from the bundled property list is copied over to the path in the documents directory so it can be modified by the application later.

The load() method from the initial code sample uses a PropertyListDecoder to decode the data that's loaded from the property list in the bundle into the APIPreferences model. If you're familiar with decoding JSON in Swift, this code should look familiar to you because it's the exact same code! Convenient, isn't it?

If we couldn't load the property list in the documents directory, or if the decoding failed, load() returns an empty object by default.

Writing a model to a property list

If you have a model that conforms to Codable as I defined in the first section of this tip, you can use a PropertyListEncoder to encode your model into data, and you can use FileManager to write that data to a plist file:

extension APIPreferencesLoader { static func write(preferences: APIPreferences) { let encoder = PropertyListEncoder() if let data = try? encoder.encode(preferences) { if FileManager.default.fileExists(atPath: plistURL.path) { // Update an existing plist try? data.write(to: plistURL) } else { // Create a new plist FileManager.default.createFile(atPath: plistURL.path, contents: data, attributes: nil) } } } }

This code checks whether our property list file exists in the documents directory using the plistURL that I defined in an earlier code snippet. If this file exists, we can simply write the encoded model's data to that file and we have successfully updated the property list in the documents directory. If the property list wasn't found in the documents directory, a new file is created with the encoded model.

Trying out the code from this post

If you've been following along, you can try the property list reading and writing quite easily with SwiftUI:

struct ContentView: View { @State private var preferences = APIPreferencesLoader.load() var body: some View { VStack { TextField("API Key", text: $preferences.apiKey) TextField("baseURL", text: $preferences.baseURL) Button("Update", action: { APIPreferencesLoader.write(preferences: self.preferences) }) } } }

If you enter some data in the text fields and press the update button, the data you entered will be persisted in a property list that's written to the document directory. When you run this example on your device or in the simulator you will find that your data is now persisted across launches.

In Summary

Because Swift contains a PropertyListEncoder and a PropertyListDecoder object, it's fairly simple to create models that can be written to a property list in Swift. This is especially true if you're already familiar with Codable and the FileManager utility. If you're not very experienced with these technologies, I hope that this Quick Tip provided you with some inspiration and ideas of what to look for, and what to explore.

If you have any feedback about this tip, or if you want to reach out to me don't hesitate to send me a tweet!