数据驱动设置界面
几乎每个应用程序都包含某种设置界面,大多数应用程序通常包含面向用户和调试设置,这些界面的实现可能会非常复杂。随着 SwiftUI 的出现,这一切都得到了简化,但我觉得通过利用数据驱动方法和 Sourcery,这些可以进一步简化。
这就是 SeveralSettings 的用武之地。
设置为
可编码
- 基于纯 Swift 值类型
- UI 由 Sourcery 生成,但开发者仍可对其进行自定义
- 更改将被跟踪,并在应用前进行审查(就像事务一样)
- 设置可以标记为需要重启或触发副作用
它是如何工作的?
您有一个设置的主结构,例如:
struct BetaSettings: AutomaticSettings {
struct Calculation: AutomaticSettings {
var mode = .linearRegression
var averageMethod = .weightedAverage
var timePeriod = 7
}
var calculation: Calculation = .init()
}
Sourcery 将分析您的结构并为每个设置和部分生成函数,您只需为主视图提供一个内容函数即可。
extension BetaSettingsView {
var content: some View {
calculationLink()
otherSectionLink()
Button("Some action that doesn't have data backing") {
// ...
}
}
}
如果您修改了数据结构,Sourcery 将更新适当的函数,并确保您的 UI 反映数据的当前状态。
调整变量的行为
可以使用 // sourcery: annotation
进行以下注释以注解变量:
skipSetting
:不为指定的变量生成 UIrequiresRestart
:如果变量发生变化,会在审查屏幕中强制应用程序重启sideEffect
:允许您在变量变化时添加副作用(例如调用外部控制器或一些函数)范围
:如果一个变量是Float/Double类型,则可以指定允许的值范围,例如:范围 = 0.0...1.0
支持类型
我们自动支持以下类型UI
字符串
布尔值
整型
浮点型
- 枚举
SettingsDisplayable
为了支持自定义枚举,你可以使用实现CaseIterable, Hashable, RawRepresentable, RawValue == String的枚举,或者你可以通过添加支持遵守SettingsDisplayable的自定义类型来添加对自定义类型的支持。对于复杂类型,你也可以为你的类型实现一个自定义的setting DSL函数。
添加新节
当你添加一个新节并用AutoSettings
进行标记时,你需要通过调用以下函数将其包含在BetaSettingsView+Content.swift
的内容函数中:
fooLink()
- 将节作为导航链接注入(仅适用于顶级节)FooView()
- 创建一个包含给定节所有设置的视图
你可以使用footerView
/ headerView
来改变它们。请注意,如果你使用injectFooter
/ injectHeader
,那么你不能为链接提供页脚和页眉,因为它们将自动调用这些函数,所以你使用一种或另一种方法。
advertisingLink(footerView: {
Button("Report Ad") {
//...
}
})
调整节行为
可以使用// sourcery: annotation
进行注释,并通过以下选项实现节注解:
injectFooter
:通过定义名为sectionNameFooter
的函数自动注入子节页脚injectHeader
:通过定义名为sectionNameHeader
的函数自动注入子节页眉
这样,你可以通过简单地添加正确定名的函数(别担心,编译器会告诉你是否命名正确)在BetaSetingsView
的扩展或节子视图中添加页眉/页脚。
当前模板支持最多两层嵌套,这意味着你可以有BetaSettings.Section.SubSection
,但如果需要,它也可以很容易地扩展到更深的嵌套层。
安装
几个步骤即可
- 您的项目需要Sourcery
- 将Sourcery 模板复制到您的项目模板中,并配置它。
- 将此库代码添加到您的项目中,您可以使用Swift Package Manager / CocoaPods,或者只需复制该项目包含的几个文件。
- 为您的设置屏幕添加视图实现(这将允许您进一步自定义它)
如果任何内容不清楚,请参考示例应用。仓库还包含突出显示功能集特定部分的提交。
配置模板
模板需要在您的 sourcery.yml
文件中设置一些参数,在 args:
部分添加它们,如下所示:
args:
settingsView: BetaSettingsView
settingsStructure: BetaSettings
settingsExternalData: BetaSettingsExternalData
settingsImports:
- FrameworkMySettingsNeed
- MyCustomFrameworkName
- OtherInternalFramework
示例视图实现
struct BetaSettingsView: View, AutomaticSettingsViewDSL {
private enum Subscreen: Swift.Identifiable {
case review
var id: String {
switch self {
case .review:
return "review"
}
}
}
@ObservedObject
var viewModel: AutomaticSettingsViewModel<BetaSettings, BetaSettingsExternalData>
@State(initialValue: nil)
private var subscreen: Subscreen?
var body: some View {
NavigationView {
content
.sheet(item: $subscreen, content: { subscreen in
Group {
switch subscreen {
case .review:
reviewScreen
}
}
})
.navigationBarTitle("Beta Settings")
.navigationBarItems(
leading: Button("Cancel") {
viewModel.cancel()
},
trailing: Group {
if viewModel.applicableChanges.isEmpty {
EmptyView()
} else {
Button("Review") {
subscreen = .review
}
}
}
)
}
}
var reviewScreen: some View {
NavigationView {
Form {
if let changes = viewModel.applicableChanges, !changes.isEmpty {
ForEach(changes) { change in
VStack {
HStack {
Text(change.name)
Spacer()
if change.requiresRestart {
Image(systemName: "goforward")
.renderingMode(.template)
.foregroundColor(.red)
}
}
HStack {
Spacer()
Text(change.change)
}
}
}
}
}
.navigationBarTitle("Review changes")
.navigationBarItems(
leading: Button("Cancel") {
subscreen = nil
},
trailing: Button("Save\(viewModel.needsRestart ? " & Restart" : "")") {
subscreen = nil
viewModel.saveChanges()
}
)
}
}
}
extension BetaSettingsView {
var content: some View {
// put your actual settings content here
}
}