Reading a Plist with Swift
Property lists are perfect for saving simple, static key-value data in your app. Here’s how you can read a plist with Swift:
func getPlist(withName name: String) -> [String]?
{
if let path = Bundle.main.path(forResource: name, ofType: "plist"),
let xml = FileManager.default.contents(atPath: path)
{
return (try? PropertyListSerialization.propertyList(from: xml, options: .mutableContainersAndLeaves, format: nil)) as? [String]
}
return nil
}
Here’s how we can use the above function:
if let fruits = getPlist(withName: "Fruit") {
print(fruits) // Output: ["Orange", "Pineapple", "Raspberry", ···]
}
What happens in the code?
- First, we’re using optional binding to get the path of the
Fruit.plist
file, and read the data from that file into thexml
constant. If any of these two lines fails,path
orxml
arenil
, and the conditional won’t execute. - Then, inside the conditional, we’re deserializing the data from
xml
as a property list. The property list is returned as an array, but we’ll have to cast it to[String]
becausepropertyList(from:options:format:)
returnsAny
. - When the conditional fails, or the type casting fails, or the
propertyList(from:options:format:)
call throws an error, the function returnsnil
. Otherwise it returns an array of type[String]?
, so when we callgetPlist(withName:)
we’re using optional binding again to get the non-optional arrayfruits
.
It’s important to point out that the type of fruit
, and the return type of getPlist(withName:)
is the same type as the data in our plist. The plist contains an array of strings, so the type of fruit
is also [String]
. You’ll always want to use the strictest possible type, such as [String: Any]
.
Warning: For the sake of simplicitly, the above code has no error handling. It’ll simply silence errors and return nil
. This is a bad practice in production apps. You’ll want to include error handling code in case the file path doesn’t exist, or if the plist is invalid and an error is thrown.
Reading a Plist with Codable
The previous approach to read a property list is far from ideal. It’s too verbose and doesn’t use strict type checking. Any change in the plist structure could potentially break your code.
Since iOS 11 you can use the Codable protocol to directly decode different data types, such as JSON, to native Swift classes and structs. And you can also use Codable
with plists!
It contains a few simple user preferences for our app, similar to what you’d put in UserDefaults.
We now need a Preferences
struct, that conforms to the Codable
protocol. Like this:
struct Preferences: Codable {
var webserviceURL:String
var itemsPerPage:Int
var backupEnabled:Bool
}
It’s important that the structs properties exactly match the keys in the plist dictionary. And note that we’re adopting the Codable
protocol.
The Preferences
struct contains some arbitrary information about backups, about a webservice URL, and how many items we want to show per page. You can put anything in your own plist, of course.
What we’ll now do is turn the Preferences.plist
file into an instance of the Preferences
struct. That’s the power of Codable
! Here’s how:
if let path = Bundle.main.path(forResource: "Preferences", ofType: "plist"),
let xml = FileManager.default.contents(atPath: path),
let preferences = try? PropertyListDecoder().decode(Preferences.self, from: xml)
{
print(preferences.webserviceURL)
}
We’re using the same boilerplate code as before. First, we’re getting the path of the plist, and then getting the Data
from the file, and assigning that to xml
.
This line is the most relevant:
let preferences = try? PropertyListDecoder().decode(Preferences.self, from: xml)
By calling the decode(_:from:)
function on an instance of PropertyListDecoder()
, the property list file gets decoded to an instance of Preferences
. We’re instructing the decoder to use that struct, with the first argument Preferences.self
. Decoding and encoding a property list uses the exact same syntax as decoding/encoding JSON, but it uses a different decoder instance.
When the decoding succeeds, we can use the preferences
object as any other ordinary Swift type:
print(preferences.webserviceURL)
Awesome!
Important: Again, we’re silencing any errors. Make sure you properly handle errors in your own apps.
Writing Data to a Plist
Can you also write a dictionary or array back to a property list file? Of course! Let’s say that the user doesn’t have a Preferences
property list yet, so we’ll create a default for them. Here’s how:
let preferences = Preferences(webserviceURL: "https://api.twitter.com", itemsPerPage: 10, backupEnabled: false)
With the above code we’re creating a new instance of Preferences
that’s not yet stored as a property list. And here’s how we save it:
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("Preferences.plist")
do {
let data = try encoder.encode(preferences)
try data.write(to: path)
} catch {
print(error)
}
This is what happens in the above code:
- First, we’re creating a
PropertyListEncoder
object and setting the output format to.xml
. (Other options are.binary
and the ancient.openStep
.) - Then, we’re creating a
URL
object with a reference to aPreferences.plist
file in the app’s document directory. This is a directory in the app’s container that we’re allowed to put files in. - Then, in the
do-try-catch
block, we’re first using theencoder
to encode thepreferences
object to aData
object. After that, we simply write the data to the file referenced bypath
.
Easy-peasy! The encoder turns the Preferences
object into data that uses the XML property list format, and writes that to a file. And later on, we can read that same file URL to get the Preferences
object back again.
The iOS SDK has a few helpful component for encoding and decoding data formats, such as PropertyListEncoder
, NSKeyedArchiver
, NSCoding
and JSONEncoder
. They’re worth checking out!
Info.plist
A configuration file in a strict text format (a property list file). It is derived from, but is not identical to, the project’s Info.plist. It contains instructions to the system about how to treat and launch the app. For example, the project’s Info.plist has a calculated bundle name derived from the product name, $(PRODUCT_NAME); in the built app’s Info.plist, this calculation has been performed, and the value reads Empty Window, which is why our app is labeled “Empty Window” on the device. Also, in conjunction with the asset catalog writing out our icon file to the app bundle’s top level, a setting has been added to the built app’s Info.plist telling the system the name of that icon file, so that the system can find it and display it as our app’s icon.