更好的世界:用定制托管对象上下文(NSManagedObjectContext)防止产生“空白”托管对象(上)

128 阅读3分钟

在这里插入图片描述

概述

用 SwiftUI + CoreData 这对“双剑合璧”的强力开发组合,我们可以事倍功半、非常 easy 的开发出界面元素丰富且背后拥有持久数据库支持的 App。

在这里插入图片描述

不过,在某些情况下它们被误用或错用也可能带来一些“藏形匿影”的顽疾。

在本篇博文中,您将学到如下内容:

  1. 省略中间托管对象让实现更简洁
  2. 稍微加一点“调料”
  3. 美中不足:”空白“托管对象”泛滥成灾“

相信学完本课后,大家一定会对自定义托管对象上下文(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-)