Introduction to macOS Plist Configuration
Property lists, commonly known as plists, are the backbone of configuration and data storage on macOS and iOS. As a developer working in the Apple ecosystem, understanding how to create, read, and manipulate plist files is essential for building robust applications that persist user preferences, application settings, and structured data.
What Is a Plist File?
A plist is a flexible, hierarchical data structure used by macOS and iOS to store configuration data. At its core, a plist is simply an XML (or binary) file that organizes data into a tree of key-value pairs. The supported data types are limited but sufficient for most configuration needs:
NSString— text stringsNSNumber— integers, floats, and booleansNSDate— dates and timestampsNSData— raw binary dataNSArray— ordered collectionsNSDictionary— unordered key-value mappings
Because plists only support these foundational types, they are predictable, easy to parse, and safe to exchange between processes. They appear throughout macOS — in Info.plist files describing app metadata, in ~/Library/Preferences/ storing user defaults, and in system daemons defining launch configurations.
Why Plists Matter
Plists matter because they are the lingua franca of macOS configuration. The operating system itself relies on them to determine which apps can open certain file types, what permissions an app requests, and how background services are scheduled. For developers, mastering plists means you can:
- Declare app capabilities and entitlements
- Persist user preferences without a database
- Configure launch agents and daemons
- Share structured configuration between apps and scripts
- Integrate cleanly with system frameworks like
NSUserDefaults
Working With Plist Files
The XML Plist Format
The most human-readable form of a plist is XML. Below is a simple example of a plist file that stores application configuration:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.1">
<dict>
<key>AppName</key>
<string>MyAwesomeApp</string>
<key>Version</key>
<string>2.1.0</string>
<key>LaunchAtLogin</key>
<true/>
<key>MaxConnections</key>
<integer>10</integer>
<key>SupportedLanguages</key>
<array>
<string>en</string>
<string>fr</string>
<string>ja</string>
</array>
<key>WindowSettings</key>
<dict>
<key>Width</key>
<real>1024.0</real>
<key>Height</key>
<real>768.0</real>
</dict>
</dict>
</plist>
Notice how each key is followed by its value, and dictionaries nest naturally to form a hierarchy. Booleans use the empty tags <true/> and <false/>, while numbers distinguish between <integer> and <real>.
Reading and Writing Plists in Swift
Apple's Foundation framework provides the PropertyListSerialization class and the Codable protocol for working with plists programmatically. The modern, type-safe approach uses Codable:
import Foundation
struct AppConfig: Codable {
let appName: String
let version: String
let launchAtLogin: Bool
let maxConnections: Int
let supportedLanguages: [String]
let windowSettings: WindowSettings
}
struct WindowSettings: Codable {
let width: Double
let height: Double
}
func loadConfig(from url: URL) -> AppConfig? {
guard let data = try? Data(contentsOf: url) else {
print("Failed to load plist data")
return nil
}
let decoder = PropertyListDecoder()
do {
return try decoder.decode(AppConfig.self, from: data)
} catch {
print("Decoding error: \(error)")
return nil
}
}
func saveConfig(_ config: AppConfig, to url: URL) {
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
do {
let data = try encoder.encode(config)
try data.write(to: url)
print("Config saved successfully")
} catch {
print("Encoding error: \(error)")
}
}
// Usage
let configURL = URL(fileURLWithPath: "config.plist")
let config = AppConfig(
appName: "MyAwesomeApp",
version: "2.1.0",
launchAtLogin: true,
maxConnections: 10,
supportedLanguages: ["en", "fr", "ja"],
windowSettings: WindowSettings(width: 1024.0, height: 768.0)
)
saveConfig(config, to: configURL)
if let loaded = loadConfig(from: configURL) {
print("Loaded app: \(loaded.appName) v\(loaded.version)")
}
Using PropertyListSerialization for Dynamic Data
When you don't have a fixed schema, PropertyListSerialization lets you work with plists as generic dictionaries:
import Foundation
func readPlist(at path: String) -> [String: Any]? {
let url = URL(fileURLWithPath: path)
guard let data = try? Data(contentsOf: url) else { return nil }
var format = PropertyListSerialization.PropertyListFormat.xml
do {
let plist = try PropertyListSerialization.propertyList(
from: data,
options: [],
format: &format
)
return plist as? [String: Any]
} catch {
print("Failed to read plist: \(error)")
return nil
}
}
func writePlist(_ dict: [String: Any], to path: String) {
let url = URL(fileURLWithPath: path)
do {
let data = try PropertyListSerialization.data(
fromPropertyList: dict,
format: .xml,
options: 0
)
try data.write(to: url)
} catch {
print("Failed to write plist: \(error)")
}
}
// Example usage
if let settings = readPlist(at: "/tmp/settings.plist") {
if let theme = settings["theme"] as? String {
print("Current theme: \(theme)")
}
}
Common Plist Use Cases on macOS
The Info.plist File
Every macOS app ships with an Info.plist that describes the app to the system. It declares the bundle identifier, version, supported document types, URL schemes, and entitlements. Here is a minimal example:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>MyAwesomeApp</string>
<key>CFBundleIdentifier</key>
<string>com.example.myawesomeapp</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.myawesomeapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
</dict>
</plist>
User Defaults and Preferences
macOS stores user preferences as plist files in ~/Library/Preferences/, named by reverse-DNS bundle identifier. The UserDefaults API is the standard way to interact with them:
import Foundation
let defaults = UserDefaults.standard
// Writing values
defaults.set("dark", forKey: "theme")
defaults.set(true, forKey: "notificationsEnabled")
defaults.set(42, forKey: "maxCacheSize")
// Reading values
let theme = defaults.string(forKey: "theme") ?? "light"
let notifications = defaults.bool(forKey: "notificationsEnabled")
let cacheSize = defaults.integer(forKey: "maxCacheSize")
print("Theme: \(theme), Notifications: \(notifications), Cache: \(cacheSize)")
// Synchronizing explicitly (rarely needed in modern macOS)
defaults.synchronize()
You can also inspect these files directly from the terminal using the defaults command:
# Read all preferences for an app
defaults read com.example.myawesomeapp
# Write a single value
defaults write com.example.myawesomeapp theme -string "dark"
# Delete a key
defaults delete com.example.myawesomeapp theme
Launch Agents and Daemons
Background services on macOS are configured using plists placed in ~/Library/LaunchAgents/ (user-level) or /Library/LaunchDaemons/ (system-level). Here is an example launch agent that runs a script every hour:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.backup-task</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/backup.sh</string>
</array>
<key>StartInterval</key>
<integer>3600</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/backup.log</string>
<key>StandardErrorPath</key>
<string>/tmp/backup.err</string>
</dict>
</plist>
Load and manage the agent using launchctl:
# Load the agent
launchctl load ~/Library/LaunchAgents/com.example.backup-task.plist
# Unload it
launchctl unload ~/Library/LaunchAgents/com.example.backup-task.plist
# Check status
launchctl list | grep backup-task
Command-Line Plist Tools
macOS ships with several built-in tools for working with plists from the shell. The most useful is plutil, which can validate, convert, and edit plist files:
# Validate a plist
plutil -lint config.plist
# Convert binary plist to XML for inspection
plutil -convert xml1 binary.plist -o readable.xml
# Convert XML plist to binary (smaller, faster)
plutil -convert binary1 config.plist
# Extract a specific value by key path
plutil -extract WindowSettings.Width raw config.plist
# Replace a value
plutil -replace MaxConnections -integer 20 config.plist
# Insert a new key
plutil -insert NewFeature -bool true config.plist
# Remove a key
plutil -remove OldFeature config.plist
The defaults command is another essential tool, particularly for reading and modifying preference plists. Combined with shell scripts, these tools let you automate configuration management across machines.
Best Practices
Choose the Right Format
Use XML plists during development because they are diff-friendly and human-readable. Switch to binary plists for shipping apps or large data sets, since binary format is more compact and faster to parse. You can convert at build time using plutil in a Run Script phase.
Validate Early and Often
A malformed plist can crash your app or prevent it from launching. Always validate plist files in your build pipeline:
# In a CI script
if ! plutil -lint "$PLIST_FILE"; then
echo "Invalid plist: $PLIST_FILE"
exit 1
fi
Use Type-Safe Models
Prefer Codable with strongly typed Swift structs over casting from [String: Any] dictionaries. Type-safe models catch errors at compile time and make your configuration code self-documenting. Reserve the dictionary approach for cases where the schema is genuinely dynamic.
Never Store Secrets in Plists
Plists are plain text (or trivially decodable binary). Never store API keys, passwords, or tokens in them. Use the macOS Keychain for sensitive credentials, and keep plists limited to non-sensitive configuration values.
Handle Missing Keys Gracefully
When reading user defaults or external plists, always provide sensible fallbacks. A missing key should never crash your app:
let refreshInterval = defaults.object(forKey: "refreshInterval") as? Double ?? 60.0
let serverURL = defaults.string(forKey: "serverURL") ?? "https://api.example.com"
Keep Info.plist Minimal
Only include keys in your Info.plist that the system actually needs to read. Custom configuration values should live in a separate plist bundled as a resource, not cluttering the system-facing metadata file.
Conclusion
Plists are a deceptively simple but powerful part of the macOS development landscape. They provide a standardized, type-safe way to persist configuration data, declare app metadata, and orchestrate system services. By understanding the XML format, leveraging Swift's Codable and PropertyListSerialization APIs, and using command-line tools like plutil and defaults, you can handle virtually any configuration scenario macOS throws at you. Follow the best practices of validating early, choosing the right format, keeping secrets out of plists, and modeling your data with type-safe structs — and your apps will be more reliable, maintainable, and secure.