Reading and writing Property List files with Codable in Swift
Tags: swift, ios
Published: 2020-03-04 09:31:11

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`:

```swift
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](https://www.avanderlee.com/swift/json-parsing-decoding/) 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.

```swift
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:

```swift
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:

```swift
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:

```swift
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](https://twitter.com/donnywals)!