概述
用 SwiftUI + CoreData 这对“双剑合璧”的强力开发组合,我们可以事倍功半、非常 easy 的开发出界面元素丰富且背后拥有持久数据库支持的 App。
不过,在某些情况下它们被误用或错用也可能带来一些“藏形匿影”的顽疾。
在本篇博文中,您将学到如下内容:
- 省略中间托管对象让实现更简洁
- 稍微加一点“调料”
- 美中不足:”空白“托管对象”泛滥成灾“
相信学完本课后,大家一定会对自定义托管对象上下文(NSManagedObjectContext)的理解更加纯熟和精进!
那还等什么呢?Let‘s go!!!;)
1. 省略中间托管对象让实现更简洁
在 SwiftUI 开发范式中,对于 CoreData 新托管对象(本例中是 Worry 对象)的创建我们可以很方便的为其“量身定做”编写一个相关的新建视图。
与其用“中间”对象来临时保存新建的托管对象 Worry ,我们不如在新建视图 NewWorryView “诞生”时就自动创建一个新的对象。
struct NewWorryView: View {
@Environment(\.managedObjectContext) var context
@Environment(\.dismiss) private var dismissor
@ObservedObject private var newWorry: Worry
init() {
newWorry = Worry.new(tmpContext)
}
}
这样做的好处是,我们可以在新建视图 body 内部直接绑定(Binding)新建 Worry 对象的各个输入属性:
var body: some View {
NavigationStack {
Form {
LabeledContent("标题") {
TextField("担忧什么?", text: $newWorry.title)
}
LabeledContent("级别") {
Picker("", selection: $newWorry.level) {
ForEach(WorryLevel.allCases) { level in
Text(level.title).tag(level)
}
}
}
LabeledContent("发生时间") {
DatePicker(date: $newWorry.occurrenceTime)
}
LabeledContent("冷却时间") {
Picker("", selection: $newWorry.coolingDays) {
ForEach(WorryCoolingDays.allCases) { days in
Text(days.title).tag(Int32(days.rawValue))
}
}
}
}
.navigationTitle("新增担忧")
.toolbar {
Button("保存") {
guard verify() else { return }
try! context.save()
dismissor()
}
}
}
}
不过,这样做会带来一个问题,那就是 CoreData 托管对象的字符串(String)等一些属性默认是可选(Optional)类型,而 SwiftUI 中很多内置视图的绑定都不允许可选值:
但先别急,我们可以很快给出解决方案:那就是创建这些视图的可选绑定版本。
2. 稍微加一点“调料”
要让 SwiftUI 中原本不支持可选值绑定的视图与我们的期望“浑然天成”,我们可以非常 easy 的创建它们的可选值绑定版本:
struct TextFieldNilable: View {
var placeholder: String?
@Binding var text: String?
init(_ placeholder: String? = nil, text: Binding<String?>) {
self.placeholder = placeholder
_text = text
}
var body: some View {
TextField(placeholder ?? "", text: .init {
text ?? ""
} set: { new in
text = new
})
}
}
struct DatePickerNilable: View {
@Binding var date: Date?
var body: some View {
DatePicker("", selection: .init(get: {
date ?? .distantFuture
}, set: {
date = $0
}), displayedComponents: [.date, .hourAndMinute])
}
}
当然,这种实现看起来略显繁琐。更好的方式是用 Swift Macro 来解决此事。
例如,我们可以写一个 @NilableBinding 宏,根据 SwiftUI 已有的内置视图自动生成“可空绑定”版本的对应视图:
@NilableBinding struct TextField {}
@NilableBinding struct DatePicker {}
至于如何灵活使用宏超出了本篇博文的讨论范畴,我之前专门针对 Swift Macro 写过一系列专门的博文,感兴趣的小伙伴可以前去观赏。
3. 美中不足:”空白“托管对象”泛滥成灾“
上面的实现看上去很 nice!不过目前它有一个致命的不足。
当我们选择取消创建上面的托管 Worry 对象时,CoreData 在内存中仍会创建“空白”的 Worry 对象:
这是由于我们在新建 Worry 的 NewWorryView 视图初始化时就“无条件”的初始化了一个 newWorry 对象,所以不管愿不愿意,我们都会在托管对象上下文中插入一个新的 Worry 托管对象。
要想解决此问题,我们有很多种办法。不过其中一种格外简单,那就是定制我们自己的托管对象上下文(Custom NSManagedObjectContext)。
我们将在下一篇博文里来实际看看到底如何创建“量身定做”的自定义托管对象上下文,以及如何解决随之带来的附加小问题,我们不见不散!
总结
在本篇博文中,我们讨论了在 SwiftUI 中新建时可能产生“空白”托管对象的问题,并初步给出解决方案。
感谢观赏,我们下一篇再见!8-)