一系列 Cocoa 控件,外观恰到好处,提供现代化的 Swift API,并且能够很好地组合在一起。
CocoaCompose 的创建是为了更容易地开发 Proxygen Mac 应用程序,这是一个用于测试应用和调试远程 API 端点的 HTTP 代理工具。
在 Xcode 中,在“项目”>“Package Dependencies”下添加 CocoaCompose。
然后如下所示导入它
import CocoaCompose
CocoaCompose 包括以下组件
以下封装器有助于在首选项窗口中布局内容
所有组件都配置为开箱即用,在 Mac 应用程序中看起来很合适,并且具有易于使用的初始化程序,并接受用于值更改的闭包。默认情况下,所有组件都设置为动态类型 NSFont.TextStyle.body
。
Box
结合了一个标题标签和一个灰色着色的包装视图。
let box = Box(title: "Title", orientation: .vertical, views: [
...
])
基本的 NSButton
,其 bezelStyle
设置为 .rounded
。可以使用标题和带有符号配置的可选图像进行配置。
let image = NSImage(systemSymbolName: "checkmark.seal.fill", accessibilityDescription: nil)
let configuration = NSImage.SymbolConfiguration(paletteColors: [.white, .systemGreen])
let button = Button(title: "Click Me", image: image, symbolConfiguration: configuration) {
// do something here ...
}
CalendarPicker
是一个 NSDatePicker
,其 datePickerStyle
设置为 .clockAndCalendar
,并且 datePickerElements
配置为 .yearMonthDay
。
使用 date
、minDate
和 maxDate
对其进行配置。
let picker = CalendarPicker(date: .now) { date in
// do something here ...
}
Checkbox
是一个 NSButton
,其 buttonType
设置为 .switch
。它接受标题和一个简单的布尔值来表示选中状态。
let checkbox = Checkbox(title: "Select something", isOn: true) { enabled in
// do something here ...
}
使用 isOn
属性访问其选中状态。
let checked = checkbox.isOn
ClockPicker
是一个 NSDatePicker
,其 datePickerStyle
设置为 .clockAndCalendar
,并且 datePickerElements
配置为 .hourMinuteSecond
或 .hourMinute
。 使用 date
、minDate
和 maxDate
初始化它。
let picker = ClockPicker(date: .now) { date in
// do something here ...
}
picker.showSeconds = true
NSColorWell
的 colorWellStyle
设置为 .default
、.minimal
或 .expanded
。 使用 color
值配置它。 请注意,其他样式选项仅在 macOS 13.0 及更高版本中可用。
let colorWell = ColorWell(color: .blue) { color in
// do something here ...
}
DatePicker
是一个 NSDatePicker
,其 datePickerStyle
设置为 .textFieldAndStepper
或 .textField
,并且 datePickerElements
配置为 .yearMonthDay
或 .yearMonth
。 使用 date
、minDate
和 maxDate
初始化它。
let picker = DatePicker(date: .now) { date in
// do something here ...
}
显示选择器的步进器。
picker.showStepper = true
显示选择器的天数。
picker.showDays = true
FontPicker
是一个 NSButton
,它使用 NSFontPanel
和 NSFontManager
来显示字体选择面板。 使用 font
和可选标题初始化它。
如果未设置按钮标题,则将使用当前选定的字体显示当前字体显示名称。
let picker = FontPicker(font: myFont) { font in
// do something here ...
}
更新所选字体。
picker.selectedFont = .preferredFont(forTextStyle: .body)
Image
是一个 NSImageView
,具有可选的 onClick
处理程序和 CGSize
。
let view = Image(image: myImage)
let view = Image(named: "App Icon")
let view = Image(systemSymbolName: "tortoise")
Label
是一个禁用背景和边框绘制的 NSTextField
。它还接受一个 NSAttributedString
作为值。
let label = Label(string: "Hello")
label.stringValue = "Hello world!"
Level
是一个 NSLevelIndicator
,其 levelIndicatorStyle
设置为 .continuousCapacity
。 使用 value
、minValue
和 maxValue
初始化它。
let level = Level(value: 0.3, minValue: 0, maxValue: 1) { value in
// do something here ...
}
PopUp
将 NSPopUpButton
和可选的尾随文本标签组合到一个控件中。使用项目数组设置它,这些项目具有标题和可选的 NSImage
,以及当前选择的索引。 对于没有选择,使用 selectedIndex
值 -1。
let popup = PopUp(items: [PopUp.Item(title: "Orange", image: image)] }, selectedIndex: 0, trailingText: "flag") { item in
// do something here ...
}
为更改的选择设置回调。
popup.onChange = { item in
// do something here ...
}
配置其项目和选定的项目。
popup.items = ["One", "Two", "Three"].map { .init(title: $0) }
popup.selectedIndex = 1
Radio
是一个垂直堆叠的 NSButton
控件,其 buttonType
设置为 .radio
。 使用可选的 selectedIndex
参数初始化此组件,其中 -1 表示未选择。
您可以在单选按钮项目之后附加一个视图的水平堆栈,以将此选项与其他控件(例如 TextField
)组合在一起。 这些尾随视图会自动为当前选定的项目启用,并为其他项目禁用。
let radio = Radio(items: [
Radio.Item(title: "First"),
Radio.Item(title: "Second", views: [
TextField(value: "30", trailingText: "seconds") { text in
// do something here ...
},
])
], selectedIndex: 0) { index, previousIndex in
// do something here ...
}
配置其选定的项目。
radio.selectedIndex = 1
ScrollView
是一个 NSScrollView
,它将 NSClipView
设置为其 contentView
,并将一个视图堆栈设置为其 documentView
。 堆栈自动使用适当的系统间距。
默认情况下,它仅垂直滚动。 设置 orientation: .horizontal
会将它的项目堆栈方向和滚动方向都切换到水平方向。
ScrollView(orientation: .vertical, views: [
...
])
Separator
是一个 NSBox
,其 boxType
设置为 .separator
。
在首选项窗口中选项的各个部分之间使用分隔符。
let separator = Separator()
Slider
是一个 NSSlider
,其 sliderType
设置为 .linear
。 使用 value
、minValue
和 maxValue
初始化它。
let slider = Slider(value: 0.3, minValue: 0, maxValue: 1) { value in
// do something here ...
}
Switch
是一个 NSSwitch
。 使用 isOn
值设置它。
let switch = Switch(isOn: true) { isOn in
// do something here ...
}
Tabs
将 NSSegmentedControl
与 Tabs.Item
列表组合在一起。 它会自动显示所选索引处的项目。
let tabs = Tabs(selectedIndex: 0, items: [
.init(title: "URI", views: [
...
]),
.init(title: "Headers", views: [
...
]),
.init(title: "Body", views: [
...
])
]) { index in
...
}
使用以下属性访问其所选索引。
tabs.selectedIndex = 2
TextField
是一个 NSTextField
,带有可选的尾随 Label
。 您应该使用适合您用例的 width
来配置它。
let textField = TextField(value: "30", trailingText: "seconds") { text in
// do something here ...
}
配置其值或占位符字符串。
textField.stringValue = "50"
textField.placeholder = "Enter name"
TextView
是一个 NSScrollView
,其中包含一个 NSTextView
作为文档视图。 它设置为禁用数据检测器和拼写校正。
let textView = TextView(text: "Example text") { text in
// do something here ...
}
配置其文本和字体,并控制是否允许编辑。
textField.text = "Another text"
textField.font = .monospacedSystemFont(ofSize: 12, weight: .regular)
textField.isEditable = false
TimePicker
是一个 NSDatePicker
,其 datePickerStyle
设置为 .textFieldAndStepper
或 .textField
,并且 datePickerElements
配置为 .hourMinuteSecond
或 .hourMinute
。 使用 date
、minDate
和 maxDate
初始化它。
let picker = TimePicker(date: .now) { date in
// do something here ...
}
显示选择器的步进器。
picker.showStepper = true
显示选择器的秒数。
picker.showSeconds = true
可以使用紧凑的代码将组件组合在一起,该代码与视觉最终结果的层次结构紧密匹配。
我们使用另外两个组件来初始化 Mac 首选项窗口的内容。
PreferenceList
接受一个区段列表,并负责它们之间适当的间距。
基本上,PreferenceList
中唯一的特殊之处在于它在其视图中查找前导标题标签,并将它们全部约束到相同的宽度。 这产生了 Mac 应用程序首选项窗口的熟悉的简洁外观(在 Ventura 中设置的恐怖之前)。
使用 PreferenceList.Style
设置它,以水平居中内容(首选项窗口内容通常居中)或将内容扩展到全宽度。
PreferenceList(views: [
...
])
PreferenceSection
接受标题、组件列表,并在该部分中的所有组件下方显示一个可选的页脚文本。 部分标题显示在部分组件的左侧,右对齐。 标题文本应以冒号结尾。
部分中的视图可以使用 orientation: .horizontal
水平放置。
PreferenceSection(
title: "Options:",
footer: "This text appears below the section.",
orientation: .vertical,
views: [
...
]
)
作为 PreferenceSection
的替代方法,如果您需要在组件上方的左对齐标题,请使用带有可选页脚文本的 PreferenceBlock
。 同样,这里的标题文本应以冒号结尾。 此布局适合内容水平填充窗口的选项窗口。
部分中的视图可以使用 orientation: .horizontal
水平放置。
PreferenceBlock(
title: "Options:",
footer: "This text appears below the block.",
orientation: .vertical,
views: [
...
]
)
PreferenceGroup
接受一个项目列表,每个项目都有一个标题和一个视图的水平堆栈。
它对于创建所有具有自己标题的选项列表(例如 PopUp
或 TextField
组件)非常有用。
PreferenceGroup(items: [
.init(title: "First:", views: [...]),
.init(title: "Second:", views: [...]),
])
以下示例使用包含多个 PreferenceSection
的 PreferenceList
初始化首选项窗口,每个部分都有自己的组件。
override func loadView() {
view = NSView()
view.wantsLayer = true
title = "Test"
let list = PreferenceList(style: .center, views: [
PreferenceSection(title: "Enable:", views: [
Switch(isOn: true) { isOn in
},
]),
PreferenceSection(title: "Choose any one:", views: [
Radio(items: [
.init(title: "One"),
.init(title: "Two", views: [
PopUp(items: ["12", "13"].map { .init(title: $0) }, selectedIndex: 0, trailingText: "points") { index, title in
}
]),
.init(title: "Three", views: [
TextField(value: "15.0", trailingText: "milliseconds", width: 50) { text in
}
])], selectedIndex: 0) { index, previousIndex in
},
]),
Separator(),
PreferenceGroup(items: [
.init(title: "First:", views: [
PopUp(items: ["One", "Two"].map { .init(title: $0) }, selectedIndex: 0) { index, title in
}
]),
.init(title: "Second:", views: [
PopUp(items: ["Foobar", "Plop"].map { .init(title: $0) }, selectedIndex: 0) { index, title in
}
]),
]),
Separator(),
PreferenceSection(title: "Test:", footer: "This here demonstrates some footer text that is shown below a section of items.", views: [
Checkbox(title: "Click me", isOn: true) { enabled in
},
Checkbox(title: "Me too", isOn: true) { enabled in
},
]),
Separator(),
PreferenceSection(title: "Start date:", orientation: .horizontal, alignment: .centerY, spacing: 20, views: [
CalendarPicker() { date in
},
ClockPicker() { date in
},
]),
Separator(),
PreferenceSection(title: "Maximum level:", views: [
Box(views: [
Level(value: 0.3) { value in
},
Slider() { value in
print("value changed to \(value)")
},
])
]),
Separator(),
PreferenceSection(title: "Body text:", views: [
FontPicker() { font in
},
ColorWell(color: .blue, style: .default) { color in
},
Image(named: "AppIcon Mac", size: CGSize(width: 50, height: 50)) {
},
]),
])
view.addSubview(list)
list.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
list.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
list.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
list.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),
list.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor, constant: -20)
])
preferredContentSize = CGSize(width: 500, height: view.fittingSize.height)
}