├── .editorconfig
├── .gitattributes
├── .gitignore
├── .swiftlint.yml
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── Preferences.xcscheme
├── Example
├── AccountsScreen.swift
├── AdvancedPreferenceViewController.swift
├── AdvancedSettingsViewController.xib
├── AppDelegate.swift
├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Base.lproj
│ └── MainMenu.xib
├── GeneralSettingsViewController.swift
├── GeneralSettingsViewController.xib
├── Info.plist
├── Settings.xcodeproj
│ ├── Preferences_Info.plist
│ ├── project.pbxproj
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── Example.xcscheme
│ │ ├── Preferences-Package.xcscheme
│ │ └── xcschememanagement.plist
├── Style+UserDefaults.swift
└── Utilities.swift
├── Package.swift
├── Sources
└── Settings
│ ├── Container.swift
│ ├── Pane.swift
│ ├── Resources
│ └── Localizable.xcstrings
│ ├── Section.swift
│ ├── SegmentedControlStyleViewController.swift
│ ├── Settings.swift
│ ├── SettingsPane.swift
│ ├── SettingsStyleController.swift
│ ├── SettingsTabViewController.swift
│ ├── SettingsWindowController.swift
│ ├── Style.swift
│ ├── ToolbarItemStyleViewController.swift
│ └── Utilities.swift
├── license
├── readme.md
├── screenshot.gif
├── segmented-control.png
└── toolbar-item.png
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.build
2 | /Packages
3 | xcuserdata
4 | project.xcworkspace
5 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | only_rules:
2 | - anyobject_protocol
3 | - array_init
4 | - block_based_kvo
5 | - class_delegate_protocol
6 | - closing_brace
7 | - closure_end_indentation
8 | - closure_parameter_position
9 | - closure_spacing
10 | - collection_alignment
11 | - colon
12 | - comma
13 | - compiler_protocol_init
14 | - computed_accessors_order
15 | - conditional_returns_on_newline
16 | - contains_over_filter_count
17 | - contains_over_filter_is_empty
18 | - contains_over_first_not_nil
19 | - contains_over_range_nil_comparison
20 | - control_statement
21 | - custom_rules
22 | - deployment_target
23 | - discarded_notification_center_observer
24 | - discouraged_assert
25 | - discouraged_direct_init
26 | - discouraged_none_name
27 | - discouraged_object_literal
28 | - discouraged_optional_boolean
29 | - discouraged_optional_collection
30 | - duplicate_enum_cases
31 | - duplicate_imports
32 | - duplicated_key_in_dictionary_literal
33 | - dynamic_inline
34 | - empty_collection_literal
35 | - empty_count
36 | - empty_enum_arguments
37 | - empty_parameters
38 | - empty_parentheses_with_trailing_closure
39 | - empty_string
40 | - empty_xctest_method
41 | - enum_case_associated_values_count
42 | - explicit_init
43 | - fallthrough
44 | - fatal_error_message
45 | - first_where
46 | - flatmap_over_map_reduce
47 | - for_where
48 | - generic_type_name
49 | - ibinspectable_in_extension
50 | - identical_operands
51 | - identifier_name
52 | - implicit_getter
53 | - implicit_return
54 | - inclusive_language
55 | - inert_defer
56 | - is_disjoint
57 | - joined_default_parameter
58 | - last_where
59 | - leading_whitespace
60 | - legacy_cggeometry_functions
61 | - legacy_constant
62 | - legacy_constructor
63 | - legacy_hashing
64 | - legacy_multiple
65 | - legacy_nsgeometry_functions
66 | - legacy_random
67 | - literal_expression_end_indentation
68 | - lower_acl_than_parent
69 | - mark
70 | - modifier_order
71 | - multiline_arguments
72 | - multiline_function_chains
73 | - multiline_literal_brackets
74 | - multiline_parameters
75 | - multiline_parameters_brackets
76 | - nimble_operator
77 | - no_extension_access_modifier
78 | - no_fallthrough_only
79 | - no_space_in_method_call
80 | - notification_center_detachment
81 | - nsobject_prefer_isequal
82 | - number_separator
83 | - opening_brace
84 | - operator_usage_whitespace
85 | - operator_whitespace
86 | - orphaned_doc_comment
87 | - overridden_super_call
88 | - prefer_self_type_over_type_of_self
89 | - prefer_zero_over_explicit_init
90 | - private_action
91 | - private_outlet
92 | - private_subject
93 | - private_unit_test
94 | - prohibited_super_call
95 | - protocol_property_accessors_order
96 | - reduce_boolean
97 | - reduce_into
98 | - redundant_discardable_let
99 | - redundant_nil_coalescing
100 | - redundant_objc_attribute
101 | - redundant_optional_initialization
102 | - redundant_set_access_control
103 | - redundant_string_enum_value
104 | - redundant_type_annotation
105 | - redundant_void_return
106 | - required_enum_case
107 | - return_arrow_whitespace
108 | - shorthand_operator
109 | - sorted_first_last
110 | - statement_position
111 | - static_operator
112 | - strong_iboutlet
113 | - superfluous_disable_command
114 | - switch_case_alignment
115 | - switch_case_on_newline
116 | - syntactic_sugar
117 | - test_case_accessibility
118 | - toggle_bool
119 | - trailing_closure
120 | - trailing_comma
121 | - trailing_newline
122 | - trailing_semicolon
123 | - trailing_whitespace
124 | - unavailable_function
125 | - unneeded_break_in_switch
126 | - unneeded_parentheses_in_closure_argument
127 | - unowned_variable_capture
128 | - untyped_error_in_catch
129 | - unused_capture_list
130 | - unused_closure_parameter
131 | - unused_control_flow_label
132 | - unused_enumerated
133 | - unused_optional_binding
134 | - unused_setter_value
135 | - valid_ibinspectable
136 | - vertical_parameter_alignment
137 | - vertical_parameter_alignment_on_call
138 | - vertical_whitespace_closing_braces
139 | - vertical_whitespace_opening_braces
140 | - void_return
141 | - xct_specific_matcher
142 | - xctfail_message
143 | - yoda_condition
144 | analyzer_rules:
145 | - capture_variable
146 | - unused_declaration
147 | - unused_import
148 | number_separator:
149 | minimum_length: 5
150 | identifier_name:
151 | max_length:
152 | warning: 100
153 | error: 100
154 | min_length:
155 | warning: 2
156 | error: 2
157 | validates_start_with_lowercase: false
158 | allowed_symbols:
159 | - '_'
160 | excluded:
161 | - 'x'
162 | - 'y'
163 | - 'z'
164 | - 'a'
165 | - 'b'
166 | - 'x1'
167 | - 'x2'
168 | - 'y1'
169 | - 'y2'
170 | - 'z2'
171 | deployment_target:
172 | macOS_deployment_target: '10.10'
173 | custom_rules:
174 | no_nsrect:
175 | regex: '\bNSRect\b'
176 | match_kinds: typeidentifier
177 | message: 'Use CGRect instead of NSRect'
178 | no_nssize:
179 | regex: '\bNSSize\b'
180 | match_kinds: typeidentifier
181 | message: 'Use CGSize instead of NSSize'
182 | no_nspoint:
183 | regex: '\bNSPoint\b'
184 | match_kinds: typeidentifier
185 | message: 'Use CGPoint instead of NSPoint'
186 | no_cgfloat:
187 | regex: '\bCGFloat\b'
188 | match_kinds: typeidentifier
189 | message: 'Use Double instead of CGFloat'
190 | no_cgfloat2:
191 | regex: '\bCGFloat\('
192 | message: 'Use Double instead of CGFloat'
193 | swiftui_state_private:
194 | regex: '@(State|StateObject|ObservedObject|EnvironmentObject)\s+var'
195 | message: 'SwiftUI @State/@StateObject/@ObservedObject/@EnvironmentObject properties should be private'
196 | swiftui_environment_private:
197 | regex: '@Environment\(\\\.\w+\)\s+var'
198 | message: 'SwiftUI @Environment properties should be private'
199 | final_class:
200 | regex: '^class [a-zA-Z\d]+[^{]+\{'
201 | message: 'Classes should be marked as final whenever possible. If you actually need it to be subclassable, just add `// swiftlint:disable:next final_class`.'
202 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/Preferences.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Example/AccountsScreen.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Settings
3 |
4 | /**
5 | Function wrapping SwiftUI into `SettingsPane`, which is mimicking view controller's default construction syntax.
6 | */
7 | let AccountsSettingsViewController: () -> SettingsPane = {
8 | /**
9 | Wrap your custom view into `Settings.Pane`, while providing necessary toolbar info.
10 | */
11 | let paneView = Settings.Pane(
12 | identifier: .accounts,
13 | title: "Accounts",
14 | toolbarIcon: NSImage(systemSymbolName: "person.crop.circle", accessibilityDescription: "Accounts settings")!
15 | ) {
16 | AccountsScreen()
17 | }
18 |
19 | return Settings.PaneHostingController(pane: paneView)
20 | }
21 |
22 | /**
23 | The main view of “Accounts” settings pane.
24 | */
25 | struct AccountsScreen: View {
26 | @State private var isOn1 = true
27 | @State private var isOn2 = false
28 | @State private var isOn3 = true
29 | @State private var selection1 = 1
30 | @State private var selection2 = 0
31 | @State private var selection3 = 0
32 | @State private var isExpanded = false
33 | private let contentWidth: Double = 450.0
34 |
35 | var body: some View {
36 | Settings.Container(contentWidth: contentWidth) {
37 | Settings.Section(title: "Permissions:") {
38 | Toggle("Allow user to administer this computer", isOn: $isOn1)
39 | Text("Administrator has root access to this machine.")
40 | .settingDescription()
41 | Toggle("Allow user to access every file", isOn: $isOn2)
42 | }
43 | Settings.Section(title: "Show scroll bars:") {
44 | Picker("", selection: $selection1) {
45 | Text("When scrolling").tag(0)
46 | Text("Always").tag(1)
47 | }
48 | .labelsHidden()
49 | .pickerStyle(.radioGroup)
50 | }
51 | Settings.Section(label: {
52 | Toggle("Some toggle", isOn: $isOn3)
53 | }) {
54 | Picker("", selection: $selection2) {
55 | Text("Automatic").tag(0)
56 | Text("Manual").tag(1)
57 | }
58 | .labelsHidden()
59 | .frame(width: 120.0)
60 | Text("Automatic mode can slow things down.")
61 | .settingDescription()
62 | }
63 | Settings.Section(title: "Preview mode:") {
64 | Picker("", selection: $selection3) {
65 | Text("Automatic").tag(0)
66 | Text("Manual").tag(1)
67 | }
68 | .labelsHidden()
69 | .frame(width: 120.0)
70 | Text("Automatic mode can slow things down.")
71 | .settingDescription()
72 | }
73 | Settings.Section(title: "Expand this pane:") {
74 | Toggle("Expand", isOn: $isExpanded)
75 | if isExpanded {
76 | ZStack(alignment: .center) {
77 | Rectangle()
78 | .fill(.gray)
79 | .frame(width: 200, height: 200)
80 | .cornerRadius(20)
81 | Text("🦄")
82 | .frame(width: 180, height: 180)
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
90 | struct AccountsScreen_Previews: PreviewProvider {
91 | static var previews: some View {
92 | AccountsScreen()
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Example/AdvancedPreferenceViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import Settings
3 |
4 | final class AdvancedSettingsViewController: NSViewController, SettingsPane {
5 | let paneIdentifier = Settings.PaneIdentifier.advanced
6 | let paneTitle = "Advanced"
7 | let toolbarItemIcon = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: "Advanced settings")!
8 |
9 | @IBOutlet private var fontLabel: NSTextField!
10 | private var font = NSFont.systemFont(ofSize: 14)
11 |
12 | override var nibName: NSNib.Name? { "AdvancedSettingsViewController" }
13 |
14 | override func viewDidLoad() {
15 | super.viewDidLoad()
16 |
17 | updateFontLabel()
18 | }
19 |
20 | @IBAction
21 | private func zoomAction(_ sender: Any) {} // swiftlint:disable:this attributes
22 |
23 | @IBAction
24 | private func showFontPanel(_ sender: Any) {
25 | let fontManager = NSFontManager.shared
26 | fontManager.setSelectedFont(font, isMultiple: false)
27 | fontManager.orderFrontFontPanel(self)
28 | }
29 |
30 | @IBAction
31 | private func changeFont(_ sender: NSFontManager) {
32 | font = sender.convert(font)
33 | updateFontLabel()
34 | }
35 |
36 | private func updateFontLabel() {
37 | fontLabel.stringValue = font.displayName ?? font.fontName
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Example/AdvancedSettingsViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
60 |
61 |
62 |
63 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
--------------------------------------------------------------------------------
/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import Settings
3 |
4 | extension Settings.PaneIdentifier {
5 | static let general = Self("general")
6 | static let accounts = Self("accounts")
7 | static let advanced = Self("advanced")
8 | }
9 |
10 | @main
11 | final class AppDelegate: NSObject, NSApplicationDelegate {
12 | @IBOutlet private var window: NSWindow!
13 |
14 | private var settingsStyle: Settings.Style {
15 | get { .settingsStyleFromUserDefaults() }
16 | set {
17 | newValue.storeInUserDefaults()
18 | }
19 | }
20 |
21 | private lazy var panes: [SettingsPane] = [
22 | GeneralSettingsViewController(),
23 | AccountsSettingsViewController(),
24 | AdvancedSettingsViewController()
25 | ]
26 |
27 | private lazy var settingsWindowController = SettingsWindowController(
28 | panes: panes,
29 | style: settingsStyle,
30 | animated: true,
31 | hidesToolbarForSingleItem: true
32 | )
33 |
34 | func applicationWillFinishLaunching(_ notification: Notification) {
35 | window.orderOut(self)
36 | }
37 |
38 | func applicationDidFinishLaunching(_ notification: Notification) {
39 | settingsWindowController.show(pane: .accounts)
40 | }
41 |
42 | @IBAction private func settingsMenuItemActionHandler(_ sender: NSMenuItem) {
43 | settingsWindowController.show()
44 | }
45 |
46 | @IBAction private func switchStyle(_ sender: Any) {
47 | settingsStyle = settingsStyle == .segmentedControl
48 | ? .toolbarItems
49 | : .segmentedControl
50 |
51 | Task {
52 | try! await NSApp.relaunch() // swiftlint:disable:this force_try
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
--------------------------------------------------------------------------------
/Example/GeneralSettingsViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import Settings
3 |
4 | final class GeneralSettingsViewController: NSViewController, SettingsPane {
5 | let paneIdentifier = Settings.PaneIdentifier.general
6 | let paneTitle = "General"
7 | let toolbarItemIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "General settings")!
8 |
9 | override var nibName: NSNib.Name? { "GeneralSettingsViewController" }
10 |
11 | override func viewDidLoad() {
12 | super.viewDidLoad()
13 |
14 | // Setup stuff here
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Example/GeneralSettingsViewController.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | A macOS feature that regularly saves your files and allows you to access previous versions. Auto Save greatly reduce the chance of losing your work in the event of a system error.
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
--------------------------------------------------------------------------------
/Example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Example/Settings.xcodeproj/Preferences_Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundleShortVersionString
16 | $(MARKETING_VERSION)
17 | CFBundleVersion
18 | $(CURRENT_PROJECT_VERSION)
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Example/Settings.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 502B68E72254947B00789D9F /* Style+UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502B68E52254944600789D9F /* Style+UserDefaults.swift */; };
11 | E3194E4122573FF3006FE775 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3194E4022573FF3006FE775 /* Utilities.swift */; };
12 | E34E9EEA20E6149B002F8F86 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9EE920E6149B002F8F86 /* AppDelegate.swift */; };
13 | E34E9EEC20E6149D002F8F86 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E34E9EEB20E6149D002F8F86 /* Assets.xcassets */; };
14 | E34E9EEF20E6149D002F8F86 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = E34E9EED20E6149D002F8F86 /* MainMenu.xib */; };
15 | E34E9EFF20E617E7002F8F86 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9EFE20E617E7002F8F86 /* GeneralSettingsViewController.swift */; };
16 | E34E9F0120E61A26002F8F86 /* GeneralSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E34E9F0020E61A26002F8F86 /* GeneralSettingsViewController.xib */; };
17 | E34E9F0320E61BC0002F8F86 /* AdvancedPreferenceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E9F0220E61BC0002F8F86 /* AdvancedPreferenceViewController.swift */; };
18 | E34E9F0520E61C06002F8F86 /* AdvancedSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E34E9F0420E61C06002F8F86 /* AdvancedSettingsViewController.xib */; };
19 | E3D37D6429C86E6D00AE5ACC /* Settings in Frameworks */ = {isa = PBXBuildFile; productRef = E3D37D6329C86E6D00AE5ACC /* Settings */; };
20 | E7059F1323C2AC7400F84762 /* AccountsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7059F1123C2AC3700F84762 /* AccountsScreen.swift */; };
21 | /* End PBXBuildFile section */
22 |
23 | /* Begin PBXCopyFilesBuildPhase section */
24 | E34E9EF920E61508002F8F86 /* Embed Frameworks */ = {
25 | isa = PBXCopyFilesBuildPhase;
26 | buildActionMask = 2147483647;
27 | dstPath = "";
28 | dstSubfolderSpec = 10;
29 | files = (
30 | );
31 | name = "Embed Frameworks";
32 | runOnlyForDeploymentPostprocessing = 0;
33 | };
34 | /* End PBXCopyFilesBuildPhase section */
35 |
36 | /* Begin PBXFileReference section */
37 | 502B68E52254944600789D9F /* Style+UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = "Style+UserDefaults.swift"; sourceTree = ""; usesTabs = 1; };
38 | E3194E4022573FF3006FE775 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Utilities.swift; sourceTree = ""; usesTabs = 1; };
39 | E34E9EE720E6149B002F8F86 /* SettingsExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SettingsExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
40 | E34E9EE920E6149B002F8F86 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; usesTabs = 1; };
41 | E34E9EEB20E6149D002F8F86 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
42 | E34E9EEE20E6149D002F8F86 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
43 | E34E9EF020E6149D002F8F86 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
44 | E34E9EFE20E617E7002F8F86 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = GeneralSettingsViewController.swift; sourceTree = ""; usesTabs = 1; };
45 | E34E9F0020E61A26002F8F86 /* GeneralSettingsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GeneralSettingsViewController.xib; sourceTree = ""; };
46 | E34E9F0220E61BC0002F8F86 /* AdvancedPreferenceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AdvancedPreferenceViewController.swift; sourceTree = ""; usesTabs = 1; };
47 | E34E9F0420E61C06002F8F86 /* AdvancedSettingsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AdvancedSettingsViewController.xib; sourceTree = ""; };
48 | E3D37D6229C86E6100AE5ACC /* Settings */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Settings; path = ..; sourceTree = ""; };
49 | E7059F1123C2AC3700F84762 /* AccountsScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AccountsScreen.swift; sourceTree = ""; tabWidth = 4; usesTabs = 1; wrapsLines = 1; };
50 | /* End PBXFileReference section */
51 |
52 | /* Begin PBXFrameworksBuildPhase section */
53 | E34E9EE420E6149B002F8F86 /* Frameworks */ = {
54 | isa = PBXFrameworksBuildPhase;
55 | buildActionMask = 2147483647;
56 | files = (
57 | E3D37D6429C86E6D00AE5ACC /* Settings in Frameworks */,
58 | );
59 | runOnlyForDeploymentPostprocessing = 0;
60 | };
61 | /* End PBXFrameworksBuildPhase section */
62 |
63 | /* Begin PBXGroup section */
64 | E340668A28ADAFF3002F8EA9 /* Frameworks */ = {
65 | isa = PBXGroup;
66 | children = (
67 | E3D37D6229C86E6100AE5ACC /* Settings */,
68 | );
69 | name = Frameworks;
70 | sourceTree = "";
71 | };
72 | E34E9EE820E6149B002F8F86 /* Example */ = {
73 | isa = PBXGroup;
74 | children = (
75 | E34E9EE920E6149B002F8F86 /* AppDelegate.swift */,
76 | 502B68E52254944600789D9F /* Style+UserDefaults.swift */,
77 | E34E9EED20E6149D002F8F86 /* MainMenu.xib */,
78 | E34E9EFE20E617E7002F8F86 /* GeneralSettingsViewController.swift */,
79 | E34E9F0020E61A26002F8F86 /* GeneralSettingsViewController.xib */,
80 | E34E9F0220E61BC0002F8F86 /* AdvancedPreferenceViewController.swift */,
81 | E34E9F0420E61C06002F8F86 /* AdvancedSettingsViewController.xib */,
82 | E7059F1123C2AC3700F84762 /* AccountsScreen.swift */,
83 | E3194E4022573FF3006FE775 /* Utilities.swift */,
84 | E34E9EEB20E6149D002F8F86 /* Assets.xcassets */,
85 | E34E9EF020E6149D002F8F86 /* Info.plist */,
86 | );
87 | name = Example;
88 | sourceTree = "";
89 | };
90 | OBJ_12 /* Products */ = {
91 | isa = PBXGroup;
92 | children = (
93 | E34E9EE720E6149B002F8F86 /* SettingsExample.app */,
94 | );
95 | name = Products;
96 | sourceTree = BUILT_PRODUCTS_DIR;
97 | };
98 | OBJ_5 = {
99 | isa = PBXGroup;
100 | children = (
101 | E34E9EE820E6149B002F8F86 /* Example */,
102 | E340668A28ADAFF3002F8EA9 /* Frameworks */,
103 | OBJ_12 /* Products */,
104 | );
105 | sourceTree = "";
106 | usesTabs = 1;
107 | };
108 | /* End PBXGroup section */
109 |
110 | /* Begin PBXNativeTarget section */
111 | E34E9EE620E6149B002F8F86 /* SettingsExample */ = {
112 | isa = PBXNativeTarget;
113 | buildConfigurationList = E34E9EF220E6149D002F8F86 /* Build configuration list for PBXNativeTarget "SettingsExample" */;
114 | buildPhases = (
115 | E34E9EE320E6149B002F8F86 /* Sources */,
116 | E34E9EE420E6149B002F8F86 /* Frameworks */,
117 | E34E9EE520E6149B002F8F86 /* Resources */,
118 | E34E9EF920E61508002F8F86 /* Embed Frameworks */,
119 | );
120 | buildRules = (
121 | );
122 | dependencies = (
123 | );
124 | name = SettingsExample;
125 | packageProductDependencies = (
126 | E3D37D6329C86E6D00AE5ACC /* Settings */,
127 | );
128 | productName = PreferencesExample;
129 | productReference = E34E9EE720E6149B002F8F86 /* SettingsExample.app */;
130 | productType = "com.apple.product-type.application";
131 | };
132 | /* End PBXNativeTarget section */
133 |
134 | /* Begin PBXProject section */
135 | OBJ_1 /* Project object */ = {
136 | isa = PBXProject;
137 | attributes = {
138 | LastSwiftUpdateCheck = 0940;
139 | LastUpgradeCheck = 1420;
140 | TargetAttributes = {
141 | E34E9EE620E6149B002F8F86 = {
142 | CreatedOnToolsVersion = 9.4.1;
143 | LastSwiftMigration = 1020;
144 | SystemCapabilities = {
145 | com.apple.Sandbox = {
146 | enabled = 0;
147 | };
148 | };
149 | };
150 | };
151 | };
152 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Settings" */;
153 | compatibilityVersion = "Xcode 13.0";
154 | developmentRegion = en;
155 | hasScannedForEncodings = 0;
156 | knownRegions = (
157 | en,
158 | Base,
159 | );
160 | mainGroup = OBJ_5;
161 | productRefGroup = OBJ_12 /* Products */;
162 | projectDirPath = "";
163 | projectRoot = "";
164 | targets = (
165 | E34E9EE620E6149B002F8F86 /* SettingsExample */,
166 | );
167 | };
168 | /* End PBXProject section */
169 |
170 | /* Begin PBXResourcesBuildPhase section */
171 | E34E9EE520E6149B002F8F86 /* Resources */ = {
172 | isa = PBXResourcesBuildPhase;
173 | buildActionMask = 2147483647;
174 | files = (
175 | E34E9EEC20E6149D002F8F86 /* Assets.xcassets in Resources */,
176 | E34E9F0520E61C06002F8F86 /* AdvancedSettingsViewController.xib in Resources */,
177 | E34E9EEF20E6149D002F8F86 /* MainMenu.xib in Resources */,
178 | E34E9F0120E61A26002F8F86 /* GeneralSettingsViewController.xib in Resources */,
179 | );
180 | runOnlyForDeploymentPostprocessing = 0;
181 | };
182 | /* End PBXResourcesBuildPhase section */
183 |
184 | /* Begin PBXSourcesBuildPhase section */
185 | E34E9EE320E6149B002F8F86 /* Sources */ = {
186 | isa = PBXSourcesBuildPhase;
187 | buildActionMask = 2147483647;
188 | files = (
189 | E34E9EFF20E617E7002F8F86 /* GeneralSettingsViewController.swift in Sources */,
190 | E34E9EEA20E6149B002F8F86 /* AppDelegate.swift in Sources */,
191 | E7059F1323C2AC7400F84762 /* AccountsScreen.swift in Sources */,
192 | E34E9F0320E61BC0002F8F86 /* AdvancedPreferenceViewController.swift in Sources */,
193 | E3194E4122573FF3006FE775 /* Utilities.swift in Sources */,
194 | 502B68E72254947B00789D9F /* Style+UserDefaults.swift in Sources */,
195 | );
196 | runOnlyForDeploymentPostprocessing = 0;
197 | };
198 | /* End PBXSourcesBuildPhase section */
199 |
200 | /* Begin PBXVariantGroup section */
201 | E34E9EED20E6149D002F8F86 /* MainMenu.xib */ = {
202 | isa = PBXVariantGroup;
203 | children = (
204 | E34E9EEE20E6149D002F8F86 /* Base */,
205 | );
206 | name = MainMenu.xib;
207 | sourceTree = "";
208 | };
209 | /* End PBXVariantGroup section */
210 |
211 | /* Begin XCBuildConfiguration section */
212 | E34E9EF320E6149D002F8F86 /* Debug */ = {
213 | isa = XCBuildConfiguration;
214 | buildSettings = {
215 | ALWAYS_SEARCH_USER_PATHS = NO;
216 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
217 | CLANG_ANALYZER_NONNULL = YES;
218 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
220 | CLANG_CXX_LIBRARY = "libc++";
221 | CLANG_ENABLE_MODULES = YES;
222 | CLANG_ENABLE_OBJC_WEAK = YES;
223 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
224 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
225 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
226 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
227 | CODE_SIGN_IDENTITY = "-";
228 | CODE_SIGN_STYLE = Automatic;
229 | COMBINE_HIDPI_IMAGES = YES;
230 | DEAD_CODE_STRIPPING = YES;
231 | DEVELOPMENT_TEAM = "";
232 | GCC_C_LANGUAGE_STANDARD = gnu11;
233 | GCC_DYNAMIC_NO_PIC = NO;
234 | GCC_PREPROCESSOR_DEFINITIONS = (
235 | "DEBUG=1",
236 | "$(inherited)",
237 | );
238 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
239 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
240 | GENERATE_INFOPLIST_FILE = YES;
241 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
242 | INFOPLIST_KEY_NSMainNibFile = MainMenu;
243 | LD_RUNPATH_SEARCH_PATHS = (
244 | "$(inherited)",
245 | "@executable_path/../Frameworks",
246 | );
247 | MTL_ENABLE_DEBUG_INFO = YES;
248 | PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.PreferencesExample;
249 | PRODUCT_NAME = "$(TARGET_NAME)";
250 | PROVISIONING_PROFILE_SPECIFIER = "";
251 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
252 | SWIFT_VERSION = 5.0;
253 | };
254 | name = Debug;
255 | };
256 | E34E9EF420E6149D002F8F86 /* Release */ = {
257 | isa = XCBuildConfiguration;
258 | buildSettings = {
259 | ALWAYS_SEARCH_USER_PATHS = NO;
260 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
261 | CLANG_ANALYZER_NONNULL = YES;
262 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
263 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
264 | CLANG_CXX_LIBRARY = "libc++";
265 | CLANG_ENABLE_MODULES = YES;
266 | CLANG_ENABLE_OBJC_WEAK = YES;
267 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
268 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
270 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
271 | CODE_SIGN_IDENTITY = "-";
272 | CODE_SIGN_STYLE = Automatic;
273 | COMBINE_HIDPI_IMAGES = YES;
274 | COPY_PHASE_STRIP = NO;
275 | DEAD_CODE_STRIPPING = YES;
276 | DEVELOPMENT_TEAM = "";
277 | ENABLE_NS_ASSERTIONS = NO;
278 | GCC_C_LANGUAGE_STANDARD = gnu11;
279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
280 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
281 | GENERATE_INFOPLIST_FILE = YES;
282 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist";
283 | INFOPLIST_KEY_NSMainNibFile = MainMenu;
284 | LD_RUNPATH_SEARCH_PATHS = (
285 | "$(inherited)",
286 | "@executable_path/../Frameworks",
287 | );
288 | MTL_ENABLE_DEBUG_INFO = NO;
289 | PRODUCT_BUNDLE_IDENTIFIER = com.sindresorhus.PreferencesExample;
290 | PRODUCT_NAME = "$(TARGET_NAME)";
291 | PROVISIONING_PROFILE_SPECIFIER = "";
292 | SWIFT_VERSION = 5.0;
293 | };
294 | name = Release;
295 | };
296 | OBJ_3 /* Debug */ = {
297 | isa = XCBuildConfiguration;
298 | buildSettings = {
299 | CLANG_ENABLE_OBJC_ARC = YES;
300 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
301 | CLANG_WARN_BOOL_CONVERSION = YES;
302 | CLANG_WARN_COMMA = YES;
303 | CLANG_WARN_CONSTANT_CONVERSION = YES;
304 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
305 | CLANG_WARN_EMPTY_BODY = YES;
306 | CLANG_WARN_ENUM_CONVERSION = YES;
307 | CLANG_WARN_INFINITE_RECURSION = YES;
308 | CLANG_WARN_INT_CONVERSION = YES;
309 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
310 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
311 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
312 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
314 | CLANG_WARN_STRICT_PROTOTYPES = YES;
315 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
316 | CLANG_WARN_UNREACHABLE_CODE = YES;
317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
318 | COMBINE_HIDPI_IMAGES = YES;
319 | COPY_PHASE_STRIP = NO;
320 | DEAD_CODE_STRIPPING = YES;
321 | DEBUG_INFORMATION_FORMAT = dwarf;
322 | DYLIB_INSTALL_NAME_BASE = "@rpath";
323 | ENABLE_NS_ASSERTIONS = YES;
324 | ENABLE_STRICT_OBJC_MSGSEND = YES;
325 | ENABLE_TESTABILITY = YES;
326 | GCC_NO_COMMON_BLOCKS = YES;
327 | GCC_OPTIMIZATION_LEVEL = 0;
328 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
329 | GCC_WARN_ABOUT_RETURN_TYPE = YES;
330 | GCC_WARN_UNDECLARED_SELECTOR = YES;
331 | GCC_WARN_UNINITIALIZED_AUTOS = YES;
332 | GCC_WARN_UNUSED_FUNCTION = YES;
333 | GCC_WARN_UNUSED_VARIABLE = YES;
334 | MACOSX_DEPLOYMENT_TARGET = 11.0;
335 | ONLY_ACTIVE_ARCH = YES;
336 | OTHER_SWIFT_FLAGS = "-DXcode";
337 | PRODUCT_NAME = "$(TARGET_NAME)";
338 | SDKROOT = macosx;
339 | SUPPORTED_PLATFORMS = macosx;
340 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE;
341 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
342 | USE_HEADERMAP = NO;
343 | };
344 | name = Debug;
345 | };
346 | OBJ_4 /* Release */ = {
347 | isa = XCBuildConfiguration;
348 | buildSettings = {
349 | CLANG_ENABLE_OBJC_ARC = YES;
350 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
351 | CLANG_WARN_BOOL_CONVERSION = YES;
352 | CLANG_WARN_COMMA = YES;
353 | CLANG_WARN_CONSTANT_CONVERSION = YES;
354 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
355 | CLANG_WARN_EMPTY_BODY = YES;
356 | CLANG_WARN_ENUM_CONVERSION = YES;
357 | CLANG_WARN_INFINITE_RECURSION = YES;
358 | CLANG_WARN_INT_CONVERSION = YES;
359 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
360 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
361 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
362 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
363 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
364 | CLANG_WARN_STRICT_PROTOTYPES = YES;
365 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
366 | CLANG_WARN_UNREACHABLE_CODE = YES;
367 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
368 | COMBINE_HIDPI_IMAGES = YES;
369 | COPY_PHASE_STRIP = YES;
370 | DEAD_CODE_STRIPPING = YES;
371 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
372 | DYLIB_INSTALL_NAME_BASE = "@rpath";
373 | ENABLE_STRICT_OBJC_MSGSEND = YES;
374 | GCC_NO_COMMON_BLOCKS = YES;
375 | GCC_OPTIMIZATION_LEVEL = s;
376 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
377 | GCC_WARN_ABOUT_RETURN_TYPE = YES;
378 | GCC_WARN_UNDECLARED_SELECTOR = YES;
379 | GCC_WARN_UNINITIALIZED_AUTOS = YES;
380 | GCC_WARN_UNUSED_FUNCTION = YES;
381 | GCC_WARN_UNUSED_VARIABLE = YES;
382 | MACOSX_DEPLOYMENT_TARGET = 11.0;
383 | OTHER_SWIFT_FLAGS = "-DXcode";
384 | PRODUCT_NAME = "$(TARGET_NAME)";
385 | SDKROOT = macosx;
386 | SUPPORTED_PLATFORMS = macosx;
387 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE;
388 | SWIFT_COMPILATION_MODE = wholemodule;
389 | SWIFT_OPTIMIZATION_LEVEL = "-O";
390 | USE_HEADERMAP = NO;
391 | };
392 | name = Release;
393 | };
394 | /* End XCBuildConfiguration section */
395 |
396 | /* Begin XCConfigurationList section */
397 | E34E9EF220E6149D002F8F86 /* Build configuration list for PBXNativeTarget "SettingsExample" */ = {
398 | isa = XCConfigurationList;
399 | buildConfigurations = (
400 | E34E9EF320E6149D002F8F86 /* Debug */,
401 | E34E9EF420E6149D002F8F86 /* Release */,
402 | );
403 | defaultConfigurationIsVisible = 0;
404 | defaultConfigurationName = Release;
405 | };
406 | OBJ_2 /* Build configuration list for PBXProject "Settings" */ = {
407 | isa = XCConfigurationList;
408 | buildConfigurations = (
409 | OBJ_3 /* Debug */,
410 | OBJ_4 /* Release */,
411 | );
412 | defaultConfigurationIsVisible = 0;
413 | defaultConfigurationName = Release;
414 | };
415 | /* End XCConfigurationList section */
416 |
417 | /* Begin XCSwiftPackageProductDependency section */
418 | E3D37D6329C86E6D00AE5ACC /* Settings */ = {
419 | isa = XCSwiftPackageProductDependency;
420 | productName = Settings;
421 | };
422 | /* End XCSwiftPackageProductDependency section */
423 | };
424 | rootObject = OBJ_1 /* Project object */;
425 | }
426 |
--------------------------------------------------------------------------------
/Example/Settings.xcodeproj/xcshareddata/xcschemes/Example.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/Example/Settings.xcodeproj/xcshareddata/xcschemes/Preferences-Package.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
44 |
50 |
51 |
52 |
53 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/Example/Settings.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | SchemeUserState
5 |
6 | Preferences-Package.xcscheme
7 |
8 |
9 | SuppressBuildableAutocreation
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/Example/Style+UserDefaults.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Settings
3 |
4 | // Helpers to write styles to and read them from UserDefaults.
5 |
6 | extension Settings.Style: RawRepresentable {
7 | public var rawValue: Int {
8 | switch self {
9 | case .toolbarItems:
10 | return 0
11 | case .segmentedControl:
12 | return 1
13 | }
14 | }
15 |
16 | public init?(rawValue: Int) {
17 | switch rawValue {
18 | case 0:
19 | self = .toolbarItems
20 | case 1:
21 | self = .segmentedControl
22 | default:
23 | return nil
24 | }
25 | }
26 | }
27 |
28 | extension Settings.Style {
29 | static let userDefaultsKey = "settingsStyle"
30 |
31 | static func settingsStyleFromUserDefaults(_ userDefaults: UserDefaults = .standard) -> Self {
32 | Self(rawValue: userDefaults.integer(forKey: userDefaultsKey))
33 | ?? .toolbarItems
34 | }
35 |
36 | func storeInUserDefaults(_ userDefaults: UserDefaults = .standard) {
37 | userDefaults.set(rawValue, forKey: Self.userDefaultsKey)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Example/Utilities.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension NSApplication {
4 | @MainActor
5 | func relaunch() async throws {
6 | let configuration = NSWorkspace.OpenConfiguration()
7 | configuration.createsNewApplicationInstance = true
8 | try await NSWorkspace.shared.openApplication(at: Bundle.main.bundleURL, configuration: configuration)
9 |
10 | NSApp.terminate(nil)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.8
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "Settings",
6 | defaultLocalization: "en",
7 | platforms: [
8 | .macOS(.v10_13)
9 | ],
10 | products: [
11 | .library(
12 | name: "Settings",
13 | targets: [
14 | "Settings"
15 | ]
16 | )
17 | ],
18 | targets: [
19 | .target(
20 | name: "Settings",
21 | resources: [
22 | .process("Resources")
23 | ]
24 | )
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Sources/Settings/Container.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @available(macOS 10.15, *)
4 | extension Settings {
5 | /**
6 | Function builder for `Settings` components used in order to restrict types of child views to be of type `Section`.
7 | */
8 | @resultBuilder
9 | public struct SectionBuilder {
10 | public static func buildBlock(_ sections: Section...) -> [Section] {
11 | sections
12 | }
13 | }
14 |
15 | /**
16 | A view which holds `Settings.Section` views and does all the alignment magic similar to `NSGridView` from AppKit.
17 | */
18 | public struct Container: View {
19 | private let sectionBuilder: () -> [Section]
20 | private let contentWidth: Double
21 | private let minimumLabelWidth: Double
22 | @State private var maximumLabelWidth = 0.0
23 |
24 | /**
25 | Creates an instance of container component, which handles layout of stacked `Settings.Section` views.
26 |
27 | Custom alignment requires content width to be specified beforehand.
28 |
29 | - Parameters:
30 | - contentWidth: A fixed width of the container's content (excluding paddings).
31 | - minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label.
32 | - builder: A view builder that creates `Settings.Section`'s of this container.
33 | */
34 | public init(
35 | contentWidth: Double,
36 | minimumLabelWidth: Double = 0,
37 | @SectionBuilder builder: @escaping () -> [Section]
38 | ) {
39 | self.sectionBuilder = builder
40 | self.contentWidth = contentWidth
41 | self.minimumLabelWidth = minimumLabelWidth
42 | }
43 |
44 | public var body: some View {
45 | let sections = sectionBuilder()
46 |
47 | return VStack(alignment: .settingsSectionLabel) {
48 | ForEach(0.. some View {
60 | sections[index]
61 | if index != sections.count - 1 && sections[index].bottomDivider {
62 | Divider()
63 | // Strangely doesn't work without width being specified. Probably because of custom alignment.
64 | .frame(width: contentWidth, height: 20)
65 | .alignmentGuide(.settingsSectionLabel) { $0[.leading] + max(minimumLabelWidth, maximumLabelWidth) }
66 | }
67 | }
68 | }
69 | }
70 |
71 | /**
72 | Extension with custom alignment guide for section title labels.
73 | */
74 | @available(macOS 10.15, *)
75 | extension HorizontalAlignment {
76 | private enum SettingsSectionLabelAlignment: AlignmentID {
77 | // swiftlint:disable:next no_cgfloat
78 | static func defaultValue(in context: ViewDimensions) -> CGFloat {
79 | context[HorizontalAlignment.leading]
80 | }
81 | }
82 |
83 | static let settingsSectionLabel = HorizontalAlignment(SettingsSectionLabelAlignment.self)
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/Settings/Pane.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /**
4 | Represents a type that can be converted to `SettingsPane`.
5 |
6 | Acts as type-eraser for `Settings.Pane`.
7 | */
8 | public protocol SettingsPaneConvertible {
9 | /**
10 | Convert `self` to equivalent `SettingsPane`.
11 | */
12 | func asSettingsPane() -> SettingsPane
13 | }
14 |
15 | @available(macOS 10.15, *)
16 | extension Settings {
17 | /**
18 | Create a SwiftUI-based settings pane.
19 |
20 | SwiftUI equivalent of the `SettingsPane` protocol.
21 | */
22 | public struct Pane: View, SettingsPaneConvertible {
23 | let identifier: PaneIdentifier
24 | let title: String
25 | let toolbarIcon: NSImage
26 | let content: Content
27 |
28 | public init(
29 | identifier: PaneIdentifier,
30 | title: String,
31 | toolbarIcon: NSImage,
32 | contentView: () -> Content
33 | ) {
34 | self.identifier = identifier
35 | self.title = title
36 | self.toolbarIcon = toolbarIcon
37 | self.content = contentView()
38 | }
39 |
40 | public var body: some View { content }
41 |
42 | public func asSettingsPane() -> SettingsPane {
43 | PaneHostingController(pane: self)
44 | }
45 | }
46 |
47 | /**
48 | Hosting controller enabling `Settings.Pane` to be used alongside AppKit `NSViewController`'s.
49 | */
50 | public final class PaneHostingController: NSHostingController, SettingsPane {
51 | public let paneIdentifier: PaneIdentifier
52 | public let paneTitle: String
53 | public let toolbarItemIcon: NSImage
54 |
55 | init(
56 | identifier: PaneIdentifier,
57 | title: String,
58 | toolbarIcon: NSImage,
59 | content: Content
60 | ) {
61 | self.paneIdentifier = identifier
62 | self.paneTitle = title
63 | self.toolbarItemIcon = toolbarIcon
64 | super.init(rootView: content)
65 | }
66 |
67 | public convenience init(pane: Pane) {
68 | self.init(
69 | identifier: pane.identifier,
70 | title: pane.title,
71 | toolbarIcon: pane.toolbarIcon,
72 | content: pane.content
73 | )
74 | }
75 |
76 | @available(*, unavailable)
77 | @objc
78 | dynamic required init?(coder: NSCoder) {
79 | fatalError("init(coder:) has not been implemented")
80 | }
81 | }
82 | }
83 |
84 | @available(macOS 10.15, *)
85 | extension View {
86 | /**
87 | Applies font and color for a label used for describing a setting.
88 | */
89 | public func settingDescription() -> some View {
90 | font(.system(size: 11.0))
91 | // TODO: Use `.foregroundStyle` when targeting macOS 12.
92 | .foregroundColor(.secondary)
93 | }
94 |
95 | /**
96 | Applies font and color for a label used for describing a setting.
97 | */
98 | @available(*, deprecated, renamed: "settingDescription")
99 | public func preferenceDescription() -> some View {
100 | settingDescription()
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Sources/Settings/Resources/Localizable.xcstrings:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0",
3 | "strings": {
4 | "settings": {
5 | "localizations": {
6 | "ko": {
7 | "stringUnit": {
8 | "state": "translated",
9 | "value": "설정"
10 | }
11 | },
12 | "uk": {
13 | "stringUnit": {
14 | "state": "translated",
15 | "value": "Параметри"
16 | }
17 | },
18 | "es-419": {
19 | "stringUnit": {
20 | "state": "translated",
21 | "value": "Ajustes"
22 | }
23 | },
24 | "hi": {
25 | "stringUnit": {
26 | "value": "समायोजन",
27 | "state": "translated"
28 | }
29 | },
30 | "hu": {
31 | "stringUnit": {
32 | "state": "translated",
33 | "value": "Beállítások"
34 | }
35 | },
36 | "th": {
37 | "stringUnit": {
38 | "value": "ค่าติดตั้ง",
39 | "state": "translated"
40 | }
41 | },
42 | "ja": {
43 | "stringUnit": {
44 | "value": "設定",
45 | "state": "translated"
46 | }
47 | },
48 | "en": {
49 | "stringUnit": {
50 | "state": "translated",
51 | "value": "Settings"
52 | }
53 | },
54 | "it": {
55 | "stringUnit": {
56 | "state": "translated",
57 | "value": "Impostazioni"
58 | }
59 | },
60 | "tr": {
61 | "stringUnit": {
62 | "value": "Ayarlar",
63 | "state": "translated"
64 | }
65 | },
66 | "id": {
67 | "stringUnit": {
68 | "state": "translated",
69 | "value": "Pengaturan"
70 | }
71 | },
72 | "ru": {
73 | "stringUnit": {
74 | "value": "Настройки",
75 | "state": "translated"
76 | }
77 | },
78 | "sk": {
79 | "stringUnit": {
80 | "state": "translated",
81 | "value": "Nastavenia"
82 | }
83 | },
84 | "ro": {
85 | "stringUnit": {
86 | "state": "translated",
87 | "value": "Configurări"
88 | }
89 | },
90 | "ar": {
91 | "stringUnit": {
92 | "value": "الإعدادات",
93 | "state": "translated"
94 | }
95 | },
96 | "pl": {
97 | "stringUnit": {
98 | "value": "Ustawienia",
99 | "state": "translated"
100 | }
101 | },
102 | "hr": {
103 | "stringUnit": {
104 | "state": "translated",
105 | "value": "Postavke"
106 | }
107 | },
108 | "nl": {
109 | "stringUnit": {
110 | "state": "translated",
111 | "value": "Instellingen"
112 | }
113 | },
114 | "fi": {
115 | "stringUnit": {
116 | "value": "Asetukset",
117 | "state": "translated"
118 | }
119 | },
120 | "zh-hans": {
121 | "stringUnit": {
122 | "value": "设置",
123 | "state": "translated"
124 | }
125 | },
126 | "da": {
127 | "stringUnit": {
128 | "value": "Indstillinger",
129 | "state": "translated"
130 | }
131 | },
132 | "fr-CA": {
133 | "stringUnit": {
134 | "state": "translated",
135 | "value": "Réglages"
136 | }
137 | },
138 | "ms": {
139 | "stringUnit": {
140 | "value": "Tetapan",
141 | "state": "translated"
142 | }
143 | },
144 | "cs": {
145 | "stringUnit": {
146 | "state": "translated",
147 | "value": "Nastavení"
148 | }
149 | },
150 | "sv": {
151 | "stringUnit": {
152 | "value": "Inställningar",
153 | "state": "translated"
154 | }
155 | },
156 | "zh-hant": {
157 | "stringUnit": {
158 | "state": "translated",
159 | "value": "設定"
160 | }
161 | },
162 | "el": {
163 | "stringUnit": {
164 | "state": "translated",
165 | "value": "Ρυθμίσεις"
166 | }
167 | },
168 | "pt-PT": {
169 | "stringUnit": {
170 | "value": "Definições",
171 | "state": "translated"
172 | }
173 | },
174 | "es": {
175 | "stringUnit": {
176 | "value": "Ajustes",
177 | "state": "translated"
178 | }
179 | },
180 | "vi": {
181 | "stringUnit": {
182 | "state": "translated",
183 | "value": "Cài đặt"
184 | }
185 | },
186 | "fr": {
187 | "stringUnit": {
188 | "value": "Réglages",
189 | "state": "translated"
190 | }
191 | },
192 | "en-GB": {
193 | "stringUnit": {
194 | "value": "Settings",
195 | "state": "translated"
196 | }
197 | },
198 | "pt": {
199 | "stringUnit": {
200 | "state": "translated",
201 | "value": "Ajustes"
202 | }
203 | },
204 | "ca": {
205 | "stringUnit": {
206 | "value": "Configuració",
207 | "state": "translated"
208 | }
209 | },
210 | "no": {
211 | "stringUnit": {
212 | "state": "translated",
213 | "value": "Innstillinger"
214 | }
215 | },
216 | "de": {
217 | "stringUnit": {
218 | "value": "Einstellungen",
219 | "state": "translated"
220 | }
221 | },
222 | "he": {
223 | "stringUnit": {
224 | "value": "הגדרות",
225 | "state": "translated"
226 | }
227 | },
228 | "en-AU": {
229 | "stringUnit": {
230 | "state": "translated",
231 | "value": "Settings"
232 | }
233 | },
234 | "zh-HK": {
235 | "stringUnit": {
236 | "value": "設定",
237 | "state": "translated"
238 | }
239 | }
240 | },
241 | "extractionState": "manual"
242 | },
243 | "preferences": {
244 | "localizations": {
245 | "hr": {
246 | "stringUnit": {
247 | "state": "translated",
248 | "value": "Postavke"
249 | }
250 | },
251 | "de": {
252 | "stringUnit": {
253 | "state": "translated",
254 | "value": "Einstellungen"
255 | }
256 | },
257 | "zh-hant": {
258 | "stringUnit": {
259 | "value": "偏好設定",
260 | "state": "translated"
261 | }
262 | },
263 | "cs": {
264 | "stringUnit": {
265 | "state": "translated",
266 | "value": "Předvolby"
267 | }
268 | },
269 | "ms": {
270 | "stringUnit": {
271 | "value": "Keutamaan",
272 | "state": "translated"
273 | }
274 | },
275 | "ko": {
276 | "stringUnit": {
277 | "state": "translated",
278 | "value": "환경설정"
279 | }
280 | },
281 | "tr": {
282 | "stringUnit": {
283 | "state": "translated",
284 | "value": "Tercihler"
285 | }
286 | },
287 | "th": {
288 | "stringUnit": {
289 | "state": "translated",
290 | "value": "การตั้งค่า"
291 | }
292 | },
293 | "el": {
294 | "stringUnit": {
295 | "state": "translated",
296 | "value": "Προτιμήσεις"
297 | }
298 | },
299 | "ar": {
300 | "stringUnit": {
301 | "state": "translated",
302 | "value": "تفضيلات"
303 | }
304 | },
305 | "pt-PT": {
306 | "stringUnit": {
307 | "state": "translated",
308 | "value": "Preferências"
309 | }
310 | },
311 | "it": {
312 | "stringUnit": {
313 | "state": "translated",
314 | "value": "Preferenze"
315 | }
316 | },
317 | "vi": {
318 | "stringUnit": {
319 | "value": "Tùy chọn",
320 | "state": "translated"
321 | }
322 | },
323 | "fr-CA": {
324 | "stringUnit": {
325 | "value": "Préférences",
326 | "state": "translated"
327 | }
328 | },
329 | "ca": {
330 | "stringUnit": {
331 | "value": "Preferències",
332 | "state": "translated"
333 | }
334 | },
335 | "zh-hans": {
336 | "stringUnit": {
337 | "state": "translated",
338 | "value": "偏好设置"
339 | }
340 | },
341 | "da": {
342 | "stringUnit": {
343 | "state": "translated",
344 | "value": "Indstillinger"
345 | }
346 | },
347 | "fr": {
348 | "stringUnit": {
349 | "value": "Préférences",
350 | "state": "translated"
351 | }
352 | },
353 | "he": {
354 | "stringUnit": {
355 | "state": "translated",
356 | "value": "העדפות"
357 | }
358 | },
359 | "fi": {
360 | "stringUnit": {
361 | "value": "Asetukset",
362 | "state": "translated"
363 | }
364 | },
365 | "sk": {
366 | "stringUnit": {
367 | "value": "Nastavenia",
368 | "state": "translated"
369 | }
370 | },
371 | "es-419": {
372 | "stringUnit": {
373 | "value": "Preferencias",
374 | "state": "translated"
375 | }
376 | },
377 | "pt": {
378 | "stringUnit": {
379 | "state": "translated",
380 | "value": "Preferências"
381 | }
382 | },
383 | "zh-HK": {
384 | "stringUnit": {
385 | "state": "translated",
386 | "value": "偏好設定"
387 | }
388 | },
389 | "sv": {
390 | "stringUnit": {
391 | "value": "Inställningar",
392 | "state": "translated"
393 | }
394 | },
395 | "id": {
396 | "stringUnit": {
397 | "state": "translated",
398 | "value": "Preferensi"
399 | }
400 | },
401 | "ru": {
402 | "stringUnit": {
403 | "value": "Настройки",
404 | "state": "translated"
405 | }
406 | },
407 | "uk": {
408 | "stringUnit": {
409 | "state": "translated",
410 | "value": "Параметри"
411 | }
412 | },
413 | "nl": {
414 | "stringUnit": {
415 | "state": "translated",
416 | "value": "Voorkeuren"
417 | }
418 | },
419 | "ja": {
420 | "stringUnit": {
421 | "value": "環境設定",
422 | "state": "translated"
423 | }
424 | },
425 | "pl": {
426 | "stringUnit": {
427 | "state": "translated",
428 | "value": "Preferencje"
429 | }
430 | },
431 | "en-GB": {
432 | "stringUnit": {
433 | "state": "translated",
434 | "value": "Preferences"
435 | }
436 | },
437 | "en-AU": {
438 | "stringUnit": {
439 | "state": "translated",
440 | "value": "Preferences"
441 | }
442 | },
443 | "hu": {
444 | "stringUnit": {
445 | "value": "Beállítások",
446 | "state": "translated"
447 | }
448 | },
449 | "ro": {
450 | "stringUnit": {
451 | "value": "Preferințe",
452 | "state": "translated"
453 | }
454 | },
455 | "hi": {
456 | "stringUnit": {
457 | "state": "translated",
458 | "value": "प्राथमिकता"
459 | }
460 | },
461 | "en": {
462 | "stringUnit": {
463 | "value": "Preferences",
464 | "state": "translated"
465 | }
466 | },
467 | "es": {
468 | "stringUnit": {
469 | "state": "translated",
470 | "value": "Preferencias"
471 | }
472 | },
473 | "no": {
474 | "stringUnit": {
475 | "state": "translated",
476 | "value": "Valg"
477 | }
478 | }
479 | },
480 | "extractionState": "manual"
481 | }
482 | },
483 | "sourceLanguage": "en"
484 | }
485 |
--------------------------------------------------------------------------------
/Sources/Settings/Section.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @available(macOS 10.15, *)
4 | extension Settings {
5 | /**
6 | Represents a section with right-aligned title and optional bottom divider.
7 | */
8 | @available(macOS 10.15, *)
9 | public struct Section: View {
10 | /**
11 | Preference key holding max width of section labels.
12 | */
13 | private struct LabelWidthPreferenceKey: PreferenceKey {
14 | typealias Value = Double
15 |
16 | static var defaultValue = 0.0
17 |
18 | static func reduce(value: inout Double, nextValue: () -> Double) {
19 | let next = nextValue()
20 | value = next > value ? next : value
21 | }
22 | }
23 |
24 | /**
25 | Convenience overlay for finding a label's dimensions using `GeometryReader`.
26 | */
27 | private struct LabelOverlay: View {
28 | var body: some View {
29 | GeometryReader { geometry in
30 | Color.clear
31 | .preference(
32 | key: LabelWidthPreferenceKey.self,
33 | value: geometry.size.width
34 | )
35 | }
36 | }
37 | }
38 |
39 | /**
40 | Convenience modifier for applying `LabelWidthPreferenceKey`.
41 | */
42 | struct LabelWidthModifier: ViewModifier {
43 | @Binding var maximumWidth: Double
44 |
45 | func body(content: Content) -> some View {
46 | content
47 | .onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in
48 | maximumWidth = newMaximumWidth
49 | }
50 | }
51 | }
52 |
53 | public let label: AnyView
54 | public let content: AnyView
55 | public let bottomDivider: Bool
56 | public let verticalAlignment: VerticalAlignment
57 |
58 | /**
59 | A section is responsible for controlling a single setting.
60 |
61 | - Parameters:
62 | - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
63 | - verticalAlignement: The vertical alignment of the section content.
64 | - label: A view describing the setting handled by this section.
65 | - content: A content view.
66 | */
67 | public init(
68 | bottomDivider: Bool = false,
69 | verticalAlignment: VerticalAlignment = .firstTextBaseline,
70 | label: @escaping () -> some View,
71 | @ViewBuilder content: @escaping () -> some View
72 | ) {
73 | self.label = label()
74 | .overlay(LabelOverlay())
75 | .eraseToAnyView() // TODO: Remove use of `AnyView`.
76 | self.bottomDivider = bottomDivider
77 | self.verticalAlignment = verticalAlignment
78 | let stack = VStack(alignment: .leading) { content() }
79 | self.content = stack.eraseToAnyView()
80 | }
81 |
82 | /**
83 | Creates instance of section, responsible for controling a single setting with `Text` as a `Label`.
84 |
85 | - Parameters:
86 | - title: A string describing the setting handled by this section.
87 | - bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
88 | - verticalAlignement: The vertical alignment of the section content.
89 | - content: A content view.
90 | */
91 | public init(
92 | title: String,
93 | bottomDivider: Bool = false,
94 | verticalAlignment: VerticalAlignment = .firstTextBaseline,
95 | @ViewBuilder content: @escaping () -> some View
96 | ) {
97 | let textLabel = {
98 | Text(title)
99 | .font(.system(size: 13.0))
100 | .overlay(LabelOverlay())
101 | .eraseToAnyView()
102 | }
103 |
104 | self.init(
105 | bottomDivider: bottomDivider,
106 | verticalAlignment: verticalAlignment,
107 | label: textLabel,
108 | content: content
109 | )
110 | }
111 |
112 | public var body: some View {
113 | HStack(alignment: verticalAlignment) {
114 | label
115 | .alignmentGuide(.settingsSectionLabel) { $0[.trailing] }
116 | content
117 | Spacer()
118 | }
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/Settings/SegmentedControlStyleViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension NSToolbarItem.Identifier {
4 | static let toolbarSegmentedControlItem = Self("toolbarSegmentedControlItem")
5 | }
6 |
7 | extension NSUserInterfaceItemIdentifier {
8 | static let toolbarSegmentedControl = Self("toolbarSegmentedControl")
9 | }
10 |
11 | final class SegmentedControlStyleViewController: NSViewController, SettingsStyleController {
12 | var segmentedControl: NSSegmentedControl! {
13 | get { view as? NSSegmentedControl }
14 | set {
15 | view = newValue
16 | }
17 | }
18 |
19 | var isKeepingWindowCentered: Bool { true }
20 |
21 | weak var delegate: SettingsStyleControllerDelegate?
22 |
23 | private var panes: [SettingsPane]!
24 |
25 | required init(panes: [SettingsPane]) {
26 | super.init(nibName: nil, bundle: nil)
27 | self.panes = panes
28 | }
29 |
30 | @available(*, unavailable)
31 | required init?(coder: NSCoder) {
32 | fatalError("init(coder:) has not been implemented")
33 | }
34 |
35 | override func loadView() {
36 | view = createSegmentedControl(panes: panes)
37 | }
38 |
39 | fileprivate func createSegmentedControl(panes: [SettingsPane]) -> NSSegmentedControl {
40 | let segmentedControl = NSSegmentedControl()
41 | segmentedControl.segmentCount = panes.count
42 | segmentedControl.segmentStyle = .texturedSquare
43 | segmentedControl.target = self
44 | segmentedControl.action = #selector(segmentedControlAction)
45 | segmentedControl.identifier = .toolbarSegmentedControl
46 |
47 | if let cell = segmentedControl.cell as? NSSegmentedCell {
48 | cell.controlSize = .regular
49 | cell.trackingMode = .selectOne
50 | }
51 |
52 | let segmentSize: CGSize = {
53 | let insets = CGSize(width: 36, height: 12)
54 | var maxSize = CGSize.zero
55 |
56 | for pane in panes {
57 | let title = pane.paneTitle
58 | let titleSize = title.size(
59 | withAttributes: [
60 | .font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular))
61 | ]
62 | )
63 |
64 | maxSize = CGSize(
65 | width: max(titleSize.width, maxSize.width),
66 | height: max(titleSize.height, maxSize.height)
67 | )
68 | }
69 |
70 | return CGSize(
71 | width: maxSize.width + insets.width,
72 | height: maxSize.height + insets.height
73 | )
74 | }()
75 |
76 | let segmentBorderWidth = Double(panes.count) + 1
77 | let segmentWidth = segmentSize.width * Double(panes.count) + segmentBorderWidth
78 | let segmentHeight = segmentSize.height
79 | segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
80 |
81 | for (index, pane) in panes.enumerated() {
82 | segmentedControl.setLabel(pane.paneTitle, forSegment: index)
83 | segmentedControl.setWidth(segmentSize.width, forSegment: index)
84 | if let cell = segmentedControl.cell as? NSSegmentedCell {
85 | cell.setTag(index, forSegment: index)
86 | }
87 | }
88 |
89 | return segmentedControl
90 | }
91 |
92 | @IBAction private func segmentedControlAction(_ control: NSSegmentedControl) {
93 | delegate?.activateTab(index: control.selectedSegment, animated: true)
94 | }
95 |
96 | func selectTab(index: Int) {
97 | segmentedControl.selectedSegment = index
98 | }
99 |
100 | func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
101 | [
102 | .flexibleSpace,
103 | .toolbarSegmentedControlItem,
104 | .flexibleSpace
105 | ]
106 | }
107 |
108 | func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem? {
109 | let toolbarItemIdentifier = paneIdentifier.toolbarItemIdentifier
110 | precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem)
111 |
112 | // When the segments outgrow the window, we need to provide a group of
113 | // NSToolbarItems with custom menu item labels and action handling for the
114 | // context menu that pops up at the right edge of the window.
115 | let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier)
116 | toolbarItemGroup.view = segmentedControl
117 | toolbarItemGroup.subitems = panes.enumerated().map { index, settingsPane -> NSToolbarItem in
118 | let item = NSToolbarItem(itemIdentifier: .init("segment-\(settingsPane.paneTitle)"))
119 | item.label = settingsPane.paneTitle
120 |
121 | let menuItem = NSMenuItem(
122 | title: settingsPane.paneTitle,
123 | action: #selector(segmentedControlMenuAction),
124 | keyEquivalent: ""
125 | )
126 | menuItem.tag = index
127 | menuItem.target = self
128 | item.menuFormRepresentation = menuItem
129 |
130 | return item
131 | }
132 |
133 | return toolbarItemGroup
134 | }
135 |
136 | @IBAction private func segmentedControlMenuAction(_ menuItem: NSMenuItem) {
137 | delegate?.activateTab(index: menuItem.tag, animated: true)
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/Settings/Settings.swift:
--------------------------------------------------------------------------------
1 | /**
2 | The namespace for this package.
3 | */
4 | public enum Settings {}
5 |
6 | /**
7 | A typealias for this package's namespace to solve the conflict with [SwiftUI.Settings](https://developer.apple.com/documentation/swiftui/settings).
8 |
9 | You can also use the following code snippet to solve the conflict:
10 |
11 | ```swift
12 | import enum Settings.Settings
13 | ```
14 | */
15 | public typealias AppSettings = Settings
16 |
17 | // TODO: Remove in the next major version.
18 | // Preserve backwards compatibility.
19 | @available(*, deprecated, renamed: "Settings")
20 | public typealias Preferences = Settings
21 | @available(*, deprecated, renamed: "SettingsPane")
22 | public typealias PreferencePane = SettingsPane
23 | @available(*, deprecated, renamed: "SettingsPaneConvertible")
24 | public typealias PreferencePaneConvertible = SettingsPaneConvertible
25 | @available(*, deprecated, renamed: "SettingsWindowController")
26 | public typealias PreferencesWindowController = SettingsWindowController
27 |
28 | @available(macOS 10.15, *)
29 | extension Settings.Pane {
30 | @available(*, deprecated, renamed: "asSettingsPane()")
31 | public func asPreferencePane() -> PreferencePane {
32 | asSettingsPane()
33 | }
34 | }
35 |
36 | extension SettingsWindowController {
37 | @available(*, deprecated, renamed: "init(panes:style:animated:hidesToolbarForSingleItem:)")
38 | public convenience init(
39 | preferencePanes: [PreferencePane],
40 | style: Settings.Style = .toolbarItems,
41 | animated: Bool = true,
42 | hidesToolbarForSingleItem: Bool = true
43 | ) {
44 | self.init(
45 | panes: preferencePanes,
46 | style: style,
47 | animated: animated,
48 | hidesToolbarForSingleItem: hidesToolbarForSingleItem)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Settings/SettingsPane.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | extension Settings {
4 | public struct PaneIdentifier: Hashable, RawRepresentable, Codable {
5 | public let rawValue: String
6 |
7 | public init(rawValue: String) {
8 | self.rawValue = rawValue
9 | }
10 | }
11 | }
12 |
13 | public protocol SettingsPane: NSViewController {
14 | var paneIdentifier: Settings.PaneIdentifier { get }
15 | var paneTitle: String { get }
16 | var toolbarItemIcon: NSImage { get }
17 | }
18 |
19 | extension SettingsPane {
20 | public var toolbarItemIdentifier: NSToolbarItem.Identifier {
21 | paneIdentifier.toolbarItemIdentifier
22 | }
23 |
24 | public var toolbarItemIcon: NSImage { .empty }
25 | }
26 |
27 | extension Settings.PaneIdentifier {
28 | public init(_ rawValue: String) {
29 | self.init(rawValue: rawValue)
30 | }
31 |
32 | public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) {
33 | self.init(rawValue: itemIdentifier.rawValue)
34 | }
35 |
36 | public var toolbarItemIdentifier: NSToolbarItem.Identifier {
37 | NSToolbarItem.Identifier(rawValue)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/Settings/SettingsStyleController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | protocol SettingsStyleController: AnyObject {
4 | var delegate: SettingsStyleControllerDelegate? { get set }
5 | var isKeepingWindowCentered: Bool { get }
6 |
7 | func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
8 | func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem?
9 | func selectTab(index: Int)
10 | }
11 |
12 | protocol SettingsStyleControllerDelegate: AnyObject {
13 | func activateTab(paneIdentifier: Settings.PaneIdentifier, animated: Bool)
14 | func activateTab(index: Int, animated: Bool)
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/Settings/SettingsTabViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | final class SettingsTabViewController: NSViewController, SettingsStyleControllerDelegate {
4 | private var activeTab: Int?
5 | private var panes = [SettingsPane]()
6 | private var style: Settings.Style?
7 | internal var settingsPanesCount: Int { panes.count }
8 | private var settingsStyleController: SettingsStyleController!
9 | private var isKeepingWindowCentered: Bool { settingsStyleController.isKeepingWindowCentered }
10 |
11 | private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] {
12 | settingsStyleController?.toolbarItemIdentifiers() ?? []
13 | }
14 |
15 | var window: NSWindow! { view.window }
16 |
17 | var isAnimated = true
18 |
19 | var activeViewController: NSViewController? {
20 | guard let activeTab else {
21 | return nil
22 | }
23 |
24 | return panes[activeTab]
25 | }
26 |
27 | override func loadView() {
28 | view = NSView()
29 | view.translatesAutoresizingMaskIntoConstraints = false
30 | }
31 |
32 | func configure(panes: [SettingsPane], style: Settings.Style) {
33 | self.panes = panes
34 | self.style = style
35 | children = panes
36 |
37 | let toolbar = NSToolbar(identifier: "SettingsToolbar")
38 | toolbar.allowsUserCustomization = false
39 | toolbar.displayMode = .iconAndLabel
40 | toolbar.showsBaselineSeparator = true
41 | toolbar.delegate = self
42 |
43 | switch style {
44 | case .segmentedControl:
45 | settingsStyleController = SegmentedControlStyleViewController(panes: panes)
46 | case .toolbarItems:
47 | settingsStyleController = ToolbarItemStyleViewController(
48 | panes: panes,
49 | toolbar: toolbar,
50 | centerToolbarItems: false
51 | )
52 | }
53 | settingsStyleController.delegate = self
54 |
55 | // Called last so that `settingsStyleController` can be asked for items.
56 | window.toolbar = toolbar
57 | }
58 |
59 | func activateTab(paneIdentifier: Settings.PaneIdentifier, animated: Bool) {
60 | guard let index = (panes.firstIndex { $0.paneIdentifier == paneIdentifier }) else {
61 | return activateTab(index: 0, animated: animated)
62 | }
63 |
64 | activateTab(index: index, animated: animated)
65 | }
66 |
67 | func activateTab(index: Int, animated: Bool) {
68 | defer {
69 | activeTab = index
70 | settingsStyleController.selectTab(index: index)
71 | updateWindowTitle(tabIndex: index)
72 | }
73 |
74 | if activeTab == nil {
75 | immediatelyDisplayTab(index: index)
76 | } else {
77 | guard index != activeTab else {
78 | return
79 | }
80 |
81 | animateTabTransition(index: index, animated: animated)
82 | }
83 | }
84 |
85 | func restoreInitialTab() {
86 | if activeTab == nil {
87 | activateTab(index: 0, animated: false)
88 | }
89 | }
90 |
91 | private func updateWindowTitle(tabIndex: Int) {
92 | window.title = {
93 | if panes.count > 1 {
94 | return panes[tabIndex].paneTitle
95 | } else {
96 | let settings: String
97 | if #available(macOS 13, *) {
98 | settings = NSLocalizedString("settings", bundle: .module, comment: "Settings")
99 | } else {
100 | settings = NSLocalizedString("preferences", bundle: .module, comment: "Preferences")
101 | }
102 |
103 | let appName = Bundle.main.appName
104 | return "\(appName) \(settings)"
105 | }
106 | }()
107 | }
108 |
109 | /**
110 | Cached constraints that pin `childViewController` views to the content view.
111 | */
112 | private var activeChildViewConstraints = [NSLayoutConstraint]()
113 |
114 | private func immediatelyDisplayTab(index: Int) {
115 | let toViewController = panes[index]
116 | view.addSubview(toViewController.view)
117 | activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
118 | setWindowFrame(for: toViewController, animated: false)
119 | }
120 |
121 | private func animateTabTransition(index: Int, animated: Bool) {
122 | guard let activeTab else {
123 | assertionFailure("animateTabTransition called before a tab was displayed; transition only works from one tab to another")
124 | immediatelyDisplayTab(index: index)
125 | return
126 | }
127 |
128 | let fromViewController = panes[activeTab]
129 | let toViewController = panes[index]
130 |
131 | // View controller animations only work on macOS 10.14 and newer.
132 | let options: NSViewController.TransitionOptions
133 | if #available(macOS 10.14, *) {
134 | options = animated && isAnimated ? [.crossfade] : []
135 | } else {
136 | options = []
137 | }
138 |
139 | view.removeConstraints(activeChildViewConstraints)
140 | toViewController.view.translatesAutoresizingMaskIntoConstraints = false
141 |
142 | transition(
143 | from: fromViewController,
144 | to: toViewController,
145 | options: options
146 | ) { [self] in
147 | if
148 | isAnimated,
149 | let toolbarItemStyleViewController = settingsStyleController as? ToolbarItemStyleViewController
150 | {
151 | toolbarItemStyleViewController.refreshPreviousSelectedItem()
152 | }
153 |
154 | activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
155 | }
156 | }
157 |
158 | override func transition(
159 | from fromViewController: NSViewController,
160 | to toViewController: NSViewController,
161 | options: NSViewController.TransitionOptions = [],
162 | completionHandler completion: (() -> Void)? = nil
163 | ) {
164 | let isAnimated = !options.isEmpty && options.isSubset(of: [
165 | .crossfade,
166 | .slideUp,
167 | .slideDown,
168 | .slideForward,
169 | .slideBackward,
170 | .slideLeft,
171 | .slideRight
172 | ])
173 |
174 | if isAnimated {
175 | NSAnimationContext.runAnimationGroup({ context in
176 | context.allowsImplicitAnimation = true
177 | context.duration = 0.25
178 | context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
179 | setWindowFrame(for: toViewController, animated: true)
180 |
181 | super.transition(
182 | from: fromViewController,
183 | to: toViewController,
184 | options: options,
185 | completionHandler: completion
186 | )
187 | }, completionHandler: nil)
188 | } else {
189 | super.transition(
190 | from: fromViewController,
191 | to: toViewController,
192 | options: options,
193 | completionHandler: completion
194 | )
195 | }
196 | }
197 |
198 | private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) {
199 | guard let window else {
200 | preconditionFailure()
201 | }
202 |
203 | let contentSize = viewController.view.fittingSize
204 |
205 | let newWindowSize = window.frameRect(forContentRect: CGRect(origin: .zero, size: contentSize)).size
206 | var frame = window.frame
207 | frame.origin.y += frame.height - newWindowSize.height
208 | frame.size = newWindowSize
209 |
210 | if isKeepingWindowCentered {
211 | let horizontalDiff = (window.frame.width - newWindowSize.width) / 2
212 | frame.origin.x += horizontalDiff
213 | }
214 |
215 | let animatableWindow = animated ? window.animator() : window
216 | animatableWindow.setFrame(frame, display: false)
217 | }
218 | }
219 |
220 | extension SettingsTabViewController: NSToolbarDelegate {
221 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
222 | toolbarItemIdentifiers
223 | }
224 |
225 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
226 | toolbarItemIdentifiers
227 | }
228 |
229 | func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
230 | style == .segmentedControl ? [] : toolbarItemIdentifiers
231 | }
232 |
233 | public func toolbar(
234 | _ toolbar: NSToolbar,
235 | itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
236 | willBeInsertedIntoToolbar flag: Bool
237 | ) -> NSToolbarItem? {
238 | if itemIdentifier == .flexibleSpace {
239 | return nil
240 | }
241 |
242 | return settingsStyleController.toolbarItem(paneIdentifier: .init(fromToolbarItemIdentifier: itemIdentifier))
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/Sources/Settings/SettingsWindowController.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | extension NSWindow.FrameAutosaveName {
4 | static let settings: NSWindow.FrameAutosaveName = "com.sindresorhus.Settings.FrameAutosaveName"
5 | }
6 |
7 | public final class SettingsWindowController: NSWindowController {
8 | private let tabViewController = SettingsTabViewController()
9 |
10 | public var isAnimated: Bool {
11 | get { tabViewController.isAnimated }
12 | set {
13 | tabViewController.isAnimated = newValue
14 | }
15 | }
16 |
17 | public var hidesToolbarForSingleItem: Bool {
18 | didSet {
19 | updateToolbarVisibility()
20 | }
21 | }
22 |
23 | private func updateToolbarVisibility() {
24 | window?.toolbar?.isVisible = (hidesToolbarForSingleItem == false)
25 | || (tabViewController.settingsPanesCount > 1)
26 | }
27 |
28 | public init(
29 | panes: [SettingsPane],
30 | style: Settings.Style = .toolbarItems,
31 | animated: Bool = true,
32 | hidesToolbarForSingleItem: Bool = true
33 | ) {
34 | precondition(!panes.isEmpty, "You need to set at least one pane")
35 |
36 | let window = UserInteractionPausableWindow(
37 | contentRect: panes[0].view.bounds,
38 | styleMask: [
39 | .titled,
40 | .closable
41 | ],
42 | backing: .buffered,
43 | defer: true
44 | )
45 | self.hidesToolbarForSingleItem = hidesToolbarForSingleItem
46 | super.init(window: window)
47 |
48 | window.contentViewController = tabViewController
49 |
50 | window.titleVisibility = {
51 | switch style {
52 | case .toolbarItems:
53 | return .visible
54 | case .segmentedControl:
55 | return panes.count <= 1 ? .visible : .hidden
56 | }
57 | }()
58 |
59 | if #available(macOS 11.0, *), style == .toolbarItems {
60 | window.toolbarStyle = .preference
61 | }
62 |
63 | tabViewController.isAnimated = animated
64 | tabViewController.configure(panes: panes, style: style)
65 | updateToolbarVisibility()
66 | }
67 |
68 | @available(*, unavailable)
69 | override public init(window: NSWindow?) {
70 | fatalError("init(window:) is not supported, use init(panes:style:animated:hidesToolbarForSingleItem:)")
71 | }
72 |
73 | @available(*, unavailable)
74 | public required init?(coder: NSCoder) {
75 | fatalError("init(coder:) is not supported, use init(panes:style:animated:hidesToolbarForSingleItem:hidesToolbarForSingleItem:)")
76 | }
77 |
78 |
79 | /**
80 | Show the settings window and brings it to front.
81 |
82 | If you pass a `Settings.PaneIdentifier`, the window will activate the corresponding tab.
83 |
84 | - Parameter paneIdentifier: Identifier of the settings pane to display, or `nil` to show the tab that was open when the user last closed the window.
85 |
86 | - Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`.
87 |
88 | - See `close()` to close the window again.
89 | - See `showWindow(_:)` to show the window without the convenience of activating the app.
90 | */
91 | public func show(pane paneIdentifier: Settings.PaneIdentifier? = nil) {
92 | if let paneIdentifier {
93 | tabViewController.activateTab(paneIdentifier: paneIdentifier, animated: false)
94 | } else {
95 | tabViewController.restoreInitialTab()
96 | }
97 |
98 | #if compiler(>=5.9) && canImport(AppKit)
99 | if #available(macOS 14, *) {
100 | NSApp.activate()
101 | } else {
102 | NSApp.activate(ignoringOtherApps: true)
103 | }
104 | #else
105 | NSApp.activate(ignoringOtherApps: true)
106 | #endif
107 |
108 | showWindow(self)
109 | restoreWindowPosition()
110 | }
111 |
112 | private func restoreWindowPosition() {
113 | guard let window else {
114 | return
115 | }
116 |
117 | window.center()
118 | window.setFrameUsingName(.settings)
119 | window.setFrameAutosaveName(.settings)
120 | }
121 | }
122 |
123 | extension SettingsWindowController {
124 | /**
125 | Returns the active pane if it responds to the given action.
126 | */
127 | override public func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
128 | if let target = super.supplementalTarget(forAction: action, sender: sender) {
129 | return target
130 | }
131 |
132 | guard let activeViewController = tabViewController.activeViewController else {
133 | return nil
134 | }
135 |
136 | if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder, target.responds(to: action) {
137 | return target
138 | }
139 |
140 | if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder, target.responds(to: action) {
141 | return target
142 | }
143 |
144 | return nil
145 | }
146 | }
147 |
148 | @available(macOS 10.15, *)
149 | extension SettingsWindowController {
150 | /**
151 | Create a settings window from only SwiftUI-based settings panes.
152 | */
153 | public convenience init(
154 | panes: [SettingsPaneConvertible],
155 | style: Settings.Style = .toolbarItems,
156 | animated: Bool = true,
157 | hidesToolbarForSingleItem: Bool = true
158 | ) {
159 | self.init(
160 | panes: panes.map { $0.asSettingsPane() },
161 | style: style,
162 | animated: animated,
163 | hidesToolbarForSingleItem: hidesToolbarForSingleItem
164 | )
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Sources/Settings/Style.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension Settings {
4 | public enum Style {
5 | case toolbarItems
6 | case segmentedControl
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/Settings/ToolbarItemStyleViewController.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | final class ToolbarItemStyleViewController: NSObject, SettingsStyleController {
4 | let toolbar: NSToolbar
5 | let centerToolbarItems: Bool
6 | let panes: [SettingsPane]
7 | var isKeepingWindowCentered: Bool { centerToolbarItems }
8 | weak var delegate: SettingsStyleControllerDelegate?
9 | private var previousSelectedItemIdentifier: NSToolbarItem.Identifier?
10 |
11 | init(panes: [SettingsPane], toolbar: NSToolbar, centerToolbarItems: Bool) {
12 | self.panes = panes
13 | self.toolbar = toolbar
14 | self.centerToolbarItems = centerToolbarItems
15 | }
16 |
17 | func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
18 | var toolbarItemIdentifiers = [NSToolbarItem.Identifier]()
19 |
20 | if centerToolbarItems {
21 | toolbarItemIdentifiers.append(.flexibleSpace)
22 | }
23 |
24 | for pane in panes {
25 | toolbarItemIdentifiers.append(pane.toolbarItemIdentifier)
26 | }
27 |
28 | if centerToolbarItems {
29 | toolbarItemIdentifiers.append(.flexibleSpace)
30 | }
31 |
32 | return toolbarItemIdentifiers
33 | }
34 |
35 | func toolbarItem(paneIdentifier: Settings.PaneIdentifier) -> NSToolbarItem? {
36 | guard let pane = (panes.first { $0.paneIdentifier == paneIdentifier }) else {
37 | preconditionFailure()
38 | }
39 |
40 | let toolbarItem = NSToolbarItem(itemIdentifier: paneIdentifier.toolbarItemIdentifier)
41 | toolbarItem.label = pane.paneTitle
42 | toolbarItem.image = pane.toolbarItemIcon
43 | toolbarItem.target = self
44 | toolbarItem.action = #selector(toolbarItemSelected)
45 | return toolbarItem
46 | }
47 |
48 | @IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
49 | delegate?.activateTab(
50 | paneIdentifier: .init(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
51 | animated: true
52 | )
53 | }
54 |
55 | func selectTab(index: Int) {
56 | toolbar.selectedItemIdentifier = panes[index].toolbarItemIdentifier
57 | }
58 |
59 | public func refreshPreviousSelectedItem() {
60 | // On macOS Sonoma, sometimes NSToolbar would preserve the
61 | // visual selected state of previous selected toolbar item during
62 | // view animation.
63 | // AppKit doesn’t seem to offer a way to refresh toolbar items.
64 | // So we manually “refresh” it.
65 | if
66 | #available(macOS 14, *),
67 | let previousSelectedItemIdentifier,
68 | let index = toolbar.items.firstIndex(where: { $0.itemIdentifier == previousSelectedItemIdentifier })
69 | {
70 | toolbar.removeItem(at: index)
71 | toolbar.insertItem(withItemIdentifier: previousSelectedItemIdentifier, at: index)
72 | }
73 |
74 | previousSelectedItemIdentifier = toolbar.selectedItemIdentifier
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/Settings/Utilities.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension NSImage {
4 | static var empty: NSImage { NSImage(size: .zero) }
5 | }
6 |
7 | extension NSView {
8 | @discardableResult
9 | func constrainToSuperviewBounds() -> [NSLayoutConstraint] {
10 | guard let superview else {
11 | preconditionFailure("superview has to be set first")
12 | }
13 |
14 | var result = [NSLayoutConstraint]()
15 | result.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self]))
16 | result.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil, views: ["subview": self]))
17 | translatesAutoresizingMaskIntoConstraints = false
18 | superview.addConstraints(result)
19 |
20 | return result
21 | }
22 | }
23 |
24 | extension NSEvent {
25 | /**
26 | Events triggered by user interaction.
27 | */
28 | static let userInteractionEvents: [EventType] = [
29 | .leftMouseDown,
30 | .leftMouseUp,
31 | .rightMouseDown,
32 | .rightMouseUp,
33 | .leftMouseDragged,
34 | .rightMouseDragged,
35 | .keyDown,
36 | .keyUp,
37 | .scrollWheel,
38 | .tabletPoint,
39 | .otherMouseDown,
40 | .otherMouseUp,
41 | .otherMouseDragged,
42 | .gesture,
43 | .magnify,
44 | .swipe,
45 | .rotate,
46 | .beginGesture,
47 | .endGesture,
48 | .smartMagnify,
49 | .pressure,
50 | .quickLook,
51 | .directTouch
52 | ]
53 |
54 | /**
55 | Whether the event was triggered by user interaction.
56 | */
57 | var isUserInteraction: Bool { Self.userInteractionEvents.contains(type) }
58 | }
59 |
60 | extension Bundle {
61 | var appName: String {
62 | string(forInfoDictionaryKey: "CFBundleDisplayName")
63 | ?? string(forInfoDictionaryKey: "CFBundleName")
64 | ?? string(forInfoDictionaryKey: "CFBundleExecutable")
65 | ?? ""
66 | }
67 |
68 | private func string(forInfoDictionaryKey key: String) -> String? {
69 | // `object(forInfoDictionaryKey:)` prefers localized info dictionary over the regular one automatically
70 | object(forInfoDictionaryKey: key) as? String
71 | }
72 | }
73 |
74 |
75 | /**
76 | A window that allows you to disable all user interactions via `isUserInteractionEnabled`.
77 |
78 | Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set.
79 | */
80 | class UserInteractionPausableWindow: NSWindow { // swiftlint:disable:this final_class
81 | var isUserInteractionEnabled = true
82 |
83 | override func sendEvent(_ event: NSEvent) {
84 | guard isUserInteractionEnabled || !event.isUserInteraction else {
85 | return
86 | }
87 |
88 | super.sendEvent(event)
89 | }
90 |
91 | override func responds(to selector: Selector!) -> Bool {
92 | // Deactivate toolbar interactions from the Main Menu.
93 | if selector == #selector(NSWindow.toggleToolbarShown(_:)) {
94 | return false
95 | }
96 |
97 | return super.responds(to: selector)
98 | }
99 | }
100 |
101 |
102 | @available(macOS 10.15, *)
103 | extension View {
104 | /**
105 | Equivalent to `.eraseToAnyPublisher()` from the Combine framework.
106 | */
107 | func eraseToAnyView() -> AnyView {
108 | AnyView(self)
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Settings
2 |
3 | > Add a settings window to your macOS app in minutes
4 |
5 |
6 |
7 | Just pass in some view controllers and this package will take care of the rest. Built-in SwiftUI support.
8 |
9 | *This package is compatible with macOS 13 and automatically uses `Settings` instead of `Preferences` in the window title on macOS 13 and later.*
10 |
11 | *This project was previously known as `Preferences`.*
12 |
13 | ## Requirements
14 |
15 | macOS 10.13 and later.
16 |
17 | ## Install
18 |
19 | Add `https://github.com/sindresorhus/Settings` in the [“Swift Package Manager” tab in Xcode](https://developer.apple.com/documentation/xcode/adding_package_dependencies_to_your_app).
20 |
21 | ## Usage
22 |
23 | *Run the `Example` Xcode project to try a live example (requires macOS 11 or later).*
24 |
25 | First, create some settings pane identifiers:
26 |
27 | ```swift
28 | import Settings
29 |
30 | extension Settings.PaneIdentifier {
31 | static let general = Self("general")
32 | static let advanced = Self("advanced")
33 | }
34 | ```
35 |
36 | Second, create a couple of view controllers for the settings panes you want. The only difference from implementing a normal view controller is that you have to add the `SettingsPane` protocol and implement the `paneIdentifier`, `toolbarItemTitle`, and `toolbarItemIcon` properties, as shown below. You can leave out `toolbarItemIcon` if you're using the `.segmentedControl` style.
37 |
38 | `GeneralSettingsViewController.swift`
39 |
40 | ```swift
41 | import Cocoa
42 | import Settings
43 |
44 | final class GeneralSettingsViewController: NSViewController, SettingsPane {
45 | let paneIdentifier = Settings.PaneIdentifier.general
46 | let paneTitle = "General"
47 | let toolbarItemIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "General settings")!
48 |
49 | override var nibName: NSNib.Name? { "GeneralSettingsViewController" }
50 |
51 | override func viewDidLoad() {
52 | super.viewDidLoad()
53 |
54 | // Setup stuff here
55 | }
56 | }
57 | ```
58 |
59 | Note: If you need to support macOS versions older than macOS 11, you have to add a [fallback for the `toolbarItemIcon`](#backwards-compatibility).
60 |
61 | `AdvancedSettingsViewController.swift`
62 |
63 | ```swift
64 | import Cocoa
65 | import Settings
66 |
67 | final class AdvancedSettingsViewController: NSViewController, SettingsPane {
68 | let paneIdentifier = Settings.PaneIdentifier.advanced
69 | let paneTitle = "Advanced"
70 | let toolbarItemIcon = NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: "Advanced settings")!
71 |
72 | override var nibName: NSNib.Name? { "AdvancedSettingsViewController" }
73 |
74 | override func viewDidLoad() {
75 | super.viewDidLoad()
76 |
77 | // Setup stuff here
78 | }
79 | }
80 | ```
81 |
82 | If you need to respond actions indirectly, the settings window controller will forward responder chain actions to the active pane if it responds to that selector.
83 |
84 | ```swift
85 | final class AdvancedSettingsViewController: NSViewController, SettingsPane {
86 | @IBOutlet private var fontLabel: NSTextField!
87 | private var selectedFont = NSFont.systemFont(ofSize: 14)
88 |
89 | @IBAction private func changeFont(_ sender: NSFontManager) {
90 | font = sender.convert(font)
91 | }
92 | }
93 | ```
94 |
95 | In the `AppDelegate`, initialize a new `SettingsWindowController` and pass it the view controllers. Then add an action outlet for the `Settings…` menu item to show the settings window.
96 |
97 | `AppDelegate.swift`
98 |
99 | ```swift
100 | import Cocoa
101 | import Settings
102 |
103 | @main
104 | final class AppDelegate: NSObject, NSApplicationDelegate {
105 | @IBOutlet private var window: NSWindow!
106 |
107 | private lazy var settingsWindowController = SettingsWindowController(
108 | panes: [
109 | GeneralSettingsViewController(),
110 | AdvancedSettingsViewController()
111 | ]
112 | )
113 |
114 | func applicationDidFinishLaunching(_ notification: Notification) {}
115 |
116 | @IBAction
117 | func settingsMenuItemActionHandler(_ sender: NSMenuItem) {
118 | settingsWindowController.show()
119 | }
120 | }
121 | ```
122 |
123 | ### Settings Tab Styles
124 |
125 | When you create the `SettingsWindowController`, you can choose between the `NSToolbarItem`-based style (default) and the `NSSegmentedControl`:
126 |
127 | ```swift
128 | // …
129 | private lazy var settingsWindowController = SettingsWindowController(
130 | panes: [
131 | GeneralSettingsViewController(),
132 | AdvancedSettingsViewController()
133 | ],
134 | style: .segmentedControl
135 | )
136 | // …
137 | ```
138 |
139 | `.toolbarItem` style:
140 |
141 | 
142 |
143 | `.segmentedControl` style:
144 |
145 | 
146 |
147 | ## API
148 |
149 | ```swift
150 | public enum Settings {}
151 |
152 | extension Settings {
153 | public enum Style {
154 | case toolbarItems
155 | case segmentedControl
156 | }
157 | }
158 |
159 | public protocol SettingsPane: NSViewController {
160 | var paneIdentifier: Settings.PaneIdentifier { get }
161 | var paneTitle: String { get }
162 | var toolbarItemIcon: NSImage { get } // Not required when using the .`segmentedControl` style
163 | }
164 |
165 | public final class SettingsWindowController: NSWindowController {
166 | init(
167 | panes: [SettingsPane],
168 | style: Settings.Style = .toolbarItems,
169 | animated: Bool = true,
170 | hidesToolbarForSingleItem: Bool = true
171 | )
172 |
173 | init(
174 | panes: [SettingsPaneConvertible],
175 | style: Settings.Style = .toolbarItems,
176 | animated: Bool = true,
177 | hidesToolbarForSingleItem: Bool = true
178 | )
179 |
180 | func show(pane: Settings.PaneIdentifier? = nil)
181 | }
182 | ```
183 |
184 | As with any `NSWindowController`, call `NSWindowController#close()` to close the settings window.
185 |
186 | ## Recommendation
187 |
188 | The easiest way to create the user interface within each pane is to use a [`NSGridView`](https://developer.apple.com/documentation/appkit/nsgridview) in Interface Builder. See the example project in this repo for a demo.
189 |
190 | ## SwiftUI support
191 |
192 | If your deployment target is macOS 10.15 or later, you can use the bundled SwiftUI components to create panes. Create a `Settings.Pane` (instead of `SettingsPane` when using AppKit) using your custom view and necessary toolbar information.
193 |
194 | Run the `Example` target in the Xcode project in this repo to see a real-world example. The `Accounts` tab is in SwiftUI.
195 |
196 | There are also some bundled convenience SwiftUI components, like [`Settings.Container`](Sources/Settings/Container.swift) and [`Settings.Section`](Sources/Settings/Section.swift) to automatically achieve similar alignment to AppKit's [`NSGridView`](https://developer.apple.com/documentation/appkit/nsgridview). And also a `.settiingDescription()` view modifier to style text as a setting description.
197 |
198 | Tip: The [`Defaults`](https://github.com/sindresorhus/Defaults#swiftui-support) package makes it very easy to persist the settings.
199 |
200 | ```swift
201 | struct CustomPane: View {
202 | var body: some View {
203 | Settings.Container(contentWidth: 450.0) {
204 | Settings.Section(title: "Section Title") {
205 | // Some view.
206 | }
207 | Settings.Section(label: {
208 | // Custom label aligned on the right side.
209 | }) {
210 | // Some view.
211 | }
212 | …
213 | }
214 | }
215 | }
216 | ```
217 |
218 | Then in the `AppDelegate`, initialize a new `SettingsWindowController` and pass it the pane views.
219 |
220 | ```swift
221 | // …
222 |
223 | private lazy var settingsWindowController = SettingsWindowController(
224 | panes: [
225 | Pane(
226 | identifier: …,
227 | title: …,
228 | toolbarIcon: NSImage(…)
229 | ) {
230 | CustomPane()
231 | },
232 | Pane(
233 | identifier: …,
234 | title: …,
235 | toolbarIcon: NSImage(…)
236 | ) {
237 | AnotherCustomPane()
238 | }
239 | ]
240 | )
241 |
242 | // …
243 | ```
244 |
245 | If you want to use SwiftUI panes alongside standard AppKit `NSViewController`'s, instead wrap the pane views into `Settings.PaneHostingController` and pass them to `SettingsWindowController` as you would with standard panes.
246 |
247 | ```swift
248 | let CustomViewSettingsPaneViewController: () -> SettingsPane = {
249 | let paneView = Settings.Pane(
250 | identifier: …,
251 | title: …,
252 | toolbarIcon: NSImage(…)
253 | ) {
254 | // Your custom view (and modifiers if needed).
255 | CustomPane()
256 | // .environmentObject(someSettingsManager)
257 | }
258 |
259 | return Settings.PaneHostingController(paneView: paneView)
260 | }
261 |
262 | // …
263 |
264 | private lazy var settingsWindowController = SettingsWindowController(
265 | panes: [
266 | GeneralSettingsViewController(),
267 | AdvancedSettingsViewController(),
268 | CustomViewSettingsPaneViewController()
269 | ],
270 | style: .segmentedControl
271 | )
272 |
273 | // …
274 | ```
275 |
276 | [Full example here.](Example/AccountsScreen.swift).
277 |
278 | ## Backwards compatibility
279 |
280 | macOS 11 and later supports SF Symbols which can be conveniently used for the toolbar icons. If you need to support older macOS versions, you have to add a fallback. Apple recommends using the same icons even for older systems. The best way to achieve this is to [export the relevant SF Symbols icons](https://github.com/davedelong/sfsymbols) to images and add them to your Asset Catalog.
281 |
282 | ## Known issues
283 |
284 | ### The settings window doesn't show
285 |
286 | 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.](https://github.com/sindresorhus/Settings/pull/28)
287 |
288 | ### There are no animations on macOS 10.13 and earlier
289 |
290 | The `animated` parameter of `SettingsWindowController.init` has no effect on macOS 10.13 or earlier as those versions don't support `NSViewController.TransitionOptions.crossfade`.
291 |
292 | ## FAQ
293 |
294 | ### How can I localize the window title?
295 |
296 | The `SettingsWindowController` adheres to the [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/patterns/settings/) and uses this set of rules to determine the window title:
297 |
298 | - **Multiple settings panes:** Uses the currently selected `paneTitle` as the window title. Localize your `paneTitle`s to get localized window titles.
299 | - **Single settings pane:** Sets the window title to `APPNAME Settings`. The app name is obtained from your app's bundle. You can localize its `Info.plist` to customize the title. The `Settings` part is taken from the "Settings…" menu item, see #12. The order of lookup for the app name from your bundle:
300 | 1. `CFBundleDisplayName`
301 | 2. `CFBundleName`
302 | 3. `CFBundleExecutable`
303 | 4. Fall back to `""` to show you're missing some settings.
304 |
305 | ### Why should I use this instead of just manually implementing it myself?
306 |
307 | It can't be that hard right? Well, turns out it is:
308 |
309 | - The recommended way is to implement it using storyboards. [But storyboards...](https://gist.github.com/iraycd/01b45c5e1be7ef6957b7) And if you want the segmented control style, you have to implement it programmatically, [which is quite complex](https://github.com/sindresorhus/Settings/blob/85f8d793050004fc0154c7f6a061412e00d13fa3/Sources/Preferences/SegmentedControlStyleViewController.swift).
310 | - [Even Apple gets it wrong, a lot.](https://twitter.com/sindresorhus/status/1113382212584464384)
311 | - You have to correctly handle [window](https://github.com/sindresorhus/Settings/commit/cc25d58a9ec379812fc8f2fd7ba48f3d35b4cbff) and [tab restoration](https://github.com/sindresorhus/Settings/commit/2bb3fc7418f3dc49b534fab986807c4e70ba78c3).
312 | - [The window title format depends on whether you have a single or multiple panes.](https://developer.apple.com/design/human-interface-guidelines/patterns/settings/)
313 | - It's difficult to get the transition animation right. A lot of apps have flaky animation between panes.
314 | - You end up having to deal with a lot of gnarly auto-layout complexities.
315 |
316 | ### How is it better than [`MASPreferences`](https://github.com/shpakovski/MASPreferences)?
317 |
318 | - Written in Swift. *(No bridging header!)*
319 | - Swifty API using a protocol.
320 | - Supports segmented control style tabs.
321 | - SwiftUI support.
322 | - Fully documented.
323 | - Adheres to the [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/patterns/settings/).
324 | - The window title is automatically localized by using the system string.
325 |
326 | ## Related
327 |
328 | - [Defaults](https://github.com/sindresorhus/Defaults) - Swifty and modern UserDefaults
329 | - [LaunchAtLogin](https://github.com/sindresorhus/LaunchAtLogin) - Add "Launch at Login" functionality to your macOS app
330 | - [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) - Add user-customizable global keyboard shortcuts to your macOS app
331 | - [DockProgress](https://github.com/sindresorhus/DockProgress) - Show progress in your app's Dock icon
332 | - [More…](https://github.com/search?q=user%3Asindresorhus+language%3Aswift+archived%3Afalse&type=repositories)
333 |
334 | You might also like Sindre's [apps](https://sindresorhus.com/apps).
335 |
336 | ## Used in these apps
337 |
338 | - [TableFlip](https://tableflipapp.com) - Visual Markdown table editor by [Christian Tietze](https://github.com/DivineDominion)
339 | - [The Archive](https://zettelkasten.de/the-archive/) - Note-taking app by [Christian Tietze](https://github.com/DivineDominion)
340 | - [Word Counter](https://wordcounterapp.com) - Measuring writer's productivity by [Christian Tietze](https://github.com/DivineDominion)
341 | - [Medis](https://getmedis.com) - A Redis GUI by [Zihua Li](https://github.com/luin)
342 | - [OK JSON](https://okjson.app) - A scriptable JSON formatter by [Francis Feng](https://github.com/francisfeng)
343 | - [Menu Helper](https://github.com/Kyle-Ye/MenuHelper) - A Finder Extension App by [Kyle-Ye](https://github.com/Kyle-Ye)
344 |
345 | Want to tell the world about your app that is using this package? Open a PR!
346 |
347 | ## Maintainers
348 |
349 | - [Sindre Sorhus](https://github.com/sindresorhus)
350 | - [Christian Tietze](https://github.com/DivineDominion)
351 |
--------------------------------------------------------------------------------
/screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sindresorhus/Settings/879ea83a7bbc6dbebf62bed8c547f090146372a6/screenshot.gif
--------------------------------------------------------------------------------
/segmented-control.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sindresorhus/Settings/879ea83a7bbc6dbebf62bed8c547f090146372a6/segmented-control.png
--------------------------------------------------------------------------------
/toolbar-item.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sindresorhus/Settings/879ea83a7bbc6dbebf62bed8c547f090146372a6/toolbar-item.png
--------------------------------------------------------------------------------