Add a preferences window to your macOS app in minutes
Just pass in some view controllers and this package will take care of the rest.
- macOS 10.10+
- Xcode 11+
- Swift 5.1+
.package(url: "https://github.com/sindresorhus/Preferences", from: "1.0.0")
github "sindresorhus/Preferences"
pod 'Preferences'
Run the PreferencesExample
target in Xcode to try a live example.
First, create some preference pane identifiers:
import Preferences
extension PreferencePane.Identifier {
static let general = Identifier("general")
static let advanced = Identifier("advanced")
}
Second, create a couple of view controllers for the preference panes you want. The only difference from implementing a normal view controller is that you have to add the PreferencePane
protocol and implement the preferencePaneIdentifier
, toolbarItemTitle
, and toolbarItemIcon
properties, as shown below. You can leave out toolbarItemIcon
if you're using the .segmentedControl
style.
GeneralPreferenceViewController.swift
import Cocoa
import Preferences
final class GeneralPreferenceViewController: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier.general
let preferencePaneTitle = "General"
let toolbarItemIcon = NSImage(named: NSImage.preferencesGeneralName)!
override var nibName: NSNib.Name? { "GeneralPreferenceViewController" }
override func viewDidLoad() {
super.viewDidLoad()
// Setup stuff here
}
}
AdvancedPreferenceViewController.swift
import Cocoa
import Preferences
final class AdvancedPreferenceViewController: NSViewController, PreferencePane {
let preferencePaneIdentifier = PreferencePane.Identifier.advanced
let preferencePaneTitle = "Advanced"
let toolbarItemIcon = NSImage(named: NSImage.advancedName)!
override var nibName: NSNib.Name? { "AdvancedPreferenceViewController" }
override func viewDidLoad() {
super.viewDidLoad()
// Setup stuff here
}
}
In the AppDelegate
, initialize a new PreferencesWindowController
and pass it the view controllers. Then add an action outlet for the Preferences…
menu item to show the preferences window.
AppDelegate.swift
import Cocoa
import Preferences
@NSApplicationMain
final class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet private var window: NSWindow!
lazy var preferencesWindowController = PreferencesWindowController(
preferencePanes: [
GeneralPreferenceViewController(),
AdvancedPreferenceViewController()
]
)
func applicationDidFinishLaunching(_ notification: Notification) {}
@IBAction
func preferencesMenuItemActionHandler(_ sender: NSMenuItem) {
preferencesWindowController.show()
}
}
When you create the PreferencesWindowController
, you can choose between the NSToolbarItem
-based style (default) and the NSSegmentedControl
:
// …
lazy var preferencesWindowController = PreferencesWindowController(
preferencePanes: [
GeneralPreferenceViewController(),
AdvancedPreferenceViewController()
],
style: .segmentedControl
)
// …
.toolbarItem
style:
.segmentedControl
style:
public protocol PreferencePane: NSViewController {
var preferencePaneIdentifier: PreferencePane.Identifier { get }
var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get } // Not required when using the .`segmentedControl` style
}
public enum PreferencesStyle {
case toolbarItems
case segmentedControl
}
public final class PreferencesWindowController: NSWindowController {
init(
preferencePanes: [PreferencePane],
style: PreferencesStyle = .toolbarItems,
animated: Bool = true,
hidesToolbarForSingleItem: Bool = true
)
func show(preferencePane: PreferencePane.Identifier? = nil)
}
As with any NSWindowController
, call NSWindowController#close()
to close the preferences window.
The easiest way to create the user interface within each pane is to use a NSGridView
in Interface Builder. See the example project in this repo for a demo.
This can happen when you are not using auto-layout or have not set a size for the view controller. You can fix this by either using auto-layout or setting an explicit size, for example, preferredContentSize
in viewDidLoad()
. We intend to fix this.
The animated
parameter of PreferencesWindowController.init
has no effect on macOS 10.13 or earlier as those versions don't support NSViewController.TransitionOptions.crossfade
.
The PreferencesWindowController
adheres to the macOS Human Interface Guidelines and uses this set of rules to determine the window title:
- Multiple preference panes: Uses the currently selected
preferencePaneTitle
as the window title. Localize yourpreferencePaneTitle
s to get localized window titles. - Single preference pane: Sets the window title to
APPNAME Preferences
. The app name is obtained from your app's bundle. You can localize itsInfo.plist
to customize the title. ThePreferences
part is taken from the "Preferences…" menu item, see #12. The order of lookup for the app name from your bundle:CFBundleDisplayName
CFBundleName
CFBundleExecutable
- Fall back to
"<Unknown App Name>"
to show you're missing some settings.
It can't be that hard right? Well, turns out it is:
- The recommended way is to implement it using storyboards. But storyboards... And if you want the segmented control style, you have to implement it programmatically, which is quite complex.
- Even Apple gets it wrong, a lot.
- You have to correctly handle window and tab restoration.
- The window title format depends on whether you have a single or multiple panes.
- It's difficult to get the transition animation right. A lot of apps have flaky animation between panes.
- You end up having to deal with a lot of gnarly auto-layout complexities.
How is it better than MASPreferences
?
- Written in Swift. (No bridging header!)
- Swifty API using a protocol.
- Supports segmented control style tabs.
- Fully documented.
- Adheres to the macOS Human Interface Guidelines.
- The window title is automatically localized by using the system string.
- Defaults - Swifty and modern UserDefaults
- LaunchAtLogin - Add "Launch at Login" functionality to your macOS app
- DockProgress - Show progress in your app's Dock icon
- More…
You might also like Sindre's apps.
- TableFlip - Visual Markdown table editor by Christian Tietze
- The Archive - Note-taking app by Christian Tietze
- Word Counter - Measuring writer's productivity by Christian Tietze
Want to tell the world about your app that is using Preferences? Open a PR!