Reading and writing Property List files with Codable in Swift
Published on: March 4, 2020You 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!