A property list file, better known as a plist file, is a specific kind of file that can store data in key-value pairs. In fact, property list files are XML files simply named differently in the Apple ecosystem. Making a parallelization, plist files are similar to dictionaries in Swift; the collection type that can also store key-value pairs of data.
The best example of a property list file, is the Info.plist in every Xcode project. It contains a big number of default settings, configuration and information regarding the app, but more entries can be added on demand. For example, in order to make use of the device camera, it’s required to add the NSCameraUserDescription key, accompanied with a String value describing the purpose of using the camera.
Property list files can be proved perfect in order to store small amounts of data that can be described as key-value pairs. Just think that user default settings (NSUserDefaults) are stored in such a file in iOS apps. And it’s really easy to perform such tasks programmatically in Swift. In this post I’ll show you how to read from and write to property list files. A further discussion with some more advanced techniques is available in the second post.
A sample Plist file and a custom type
To start, suppose that we have the following property list file in the app’s bundle called DefaultShape.plist. Its purpose is to contain the default data regarding hypothetical shape types:
Contained values are of various data types:
- width, height, borderWidth and cornerRadius are Numbers.
- backgroundColor and borderColor are Strings representing colors as Hex values.
- isInteractive is a Boolean value, indicating whether a shape will be interacting on user actions or not.
- patternImage is a Data value and its purpose is to store the actual data of an image that can be used as a background pattern on the shape.
Say now that we have the following type that represents a custom shape. It has properties similar to the keys of the property list, and that’s important so things work almost automatically next in this post:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct CustomShape { var width: CGFloat = 0 var height: CGFloat = 0 var backgroundColor: String = “000000” var borderColor: String = “000000” var borderWidth: CGFloat = 0 var cornerRadius: CGFloat = 0 var isInteractive: Bool = false var patternImage: Data = Data() } |
The goal of this post is to initialize a CustomShape instance loading the plist contents and storing them to the respective properties. Notice that all of them have initial values so we avoid errors if initializing with the plist contents fails.
Decoding a property list with Codable type
Codable is not a real protocol itself in Swift. Instead, it’s the combination of two other protocols, Encodable and Decodable. With those two, it’s easy to encode and decode a native custom Swift type to and from a specific data format respectively, such as JSON and property list.
In order to manage to load plist file contents to a CustomShape instance, it’s necessary to decode the property list to that type. To achieve that, we’ll make CustomShape conform to Codable:
1 2 3 4 5 |
struct CustomShape: Codable { … } |
Note that if you want only to decode, then instead of Codable you can adopt the Decodable protocol and avoid the additional conformance to Encodable that Codable brings along. The same applies in case you want to encode only; use Encodable and not Codable. Here I’m using Codable because I’ll show later how to encode a CustomShape instance to a property list file.
Next, let’s define the following custom initializer method that will be containing a do-catch
statement. That’s because we’ll call methods that may throw an exception:
1 2 3 4 5 6 7 8 9 |
init(withPlistAt url: URL) { do { } catch { print(error.localizedDescription) } } |
The argument it expects is a URL to a property list file. I choose to implement it that way because it’s a more general solution that can work for plist files existing in both the app bundle and other locations, such as the documents directory.
Note that just printing the potential error is not a proper error handling. In real apps a more meaningful and purposeful treatment should be implemented in the catch
block.
The next step is to load the plist contents, and decode them to a CustomShape type. We manage the former by initializing a Data object using the provided URL. Note that it’s an operation marked with the try
keyword as it can throw an exception:
1 2 3 |
let plistData = try Data(contentsOf: url) |
With the plistData
object handy, we can decode the plist contents to a CustomShape instance. We will initialize an object of a class called PropertyListDecoder
in order to do that, and through that we’ll access the following method that will do the job:
1 2 3 |
self = try PropertyListDecoder().decode(CustomShape.self, from: plistData) |
The first argument we provide to the decode(_:from:)
method is the type that we want to decode to. The second is the original property list data as a Data object.
The above returns a CustomShape object, which is being assigned to the current instance represented by the self
keyword.
And that’s all! It’s that easy to initialize an object with the contents of a property list file.
Here’s the entire init method:
1 2 3 4 5 6 7 8 9 10 |
init(withPlistAt url: URL) { do { let plistData = try Data(contentsOf: url) self = try PropertyListDecoder().decode(CustomShape.self, from: plistData) } catch { print(error.localizedDescription) } } |
To use the above, first it’s necessary to get the URL to the DefaultShape.plist file in the app bundle, and then create a CustomShape instance using the custom initializer:
1 2 3 4 5 |
guard let defaultShapeURL = Bundle.main.url(forResource: “DefaultShape”, withExtension: “plist”) else { return } var shape = CustomShape(withPlistAt: defaultShapeURL) |
Encoding to property list
Let’s see now how we can perform the opposite and save an instance of the CustomShape type as a plist file. Suppose that we set new values to some properties which we want to store persistently:
1 2 3 4 5 6 7 |
shape.width = 500 shape.height = 250 shape.backgroundColor = “ff0000” shape.borderWidth = 5 shape.cornerRadius = 15 |
For that purpose, let’s define the following method in the CustomShape structure:
1 2 3 4 5 |
func save(as name: String) { } |
The provided argument will be the name we want to give to the generated file. Obviously, that file is going to have the .plist extension.
To keep things simple, let’s say that we will save to documents directory of the app. The first thing in the save(as:)
method is to compose the URL of the file that we’ll save to:
1 2 3 4 5 6 7 8 |
func save(as name: String) { let saveURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent(name) .appendingPathExtension(“plist”) } |
The first line returns the URL to the documents directory. The second appends the given file name to the URL, and the last one appends the .plist extension.
In a do-catch
once again, at first we’ll encode the current CustomShape instance (self
) to a property list data. We will do so using the PropertyListEncoder
class and the encode(_:)
method:
1 2 3 |
let encodedData = try PropertyListEncoder().encode(self) |
It’s so simple; a single line only!
The resulting encodedData
object is a Data instance, and we can write it on disk like so:
1 2 3 |
try encodedData.write(to: saveURL) |
Here’s the save(as:)
method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func save(as name: String) { let saveURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent(name) .appendingPathExtension(“plist”) do { let encodedData = try PropertyListEncoder().encode(self) try encodedData.write(to: saveURL) } catch { print(error.localizedDescription) } } |
The above method can now be used as follows:
1 2 3 |
shape.save(as: “myShape”) |
Summary
In this post I showed you how to decode data from property list files into a custom type, and encode and save back to plist as well. All that using the Codable type, PropertyListEncoder and PropertyListDecoder APIs. However, things are not always ideal, and you may want to avoid including all properties in an encoding or decoding process. Or, you may have properties of data types that cannot be encoded or decoded at all. All that are being discussed in the second post about property lists in Swift. Don’t miss it!