本日的文章中,我们将新建一个app,实现宠物体重数据的记录并保存到icloud,主要分为数据的持久化保存和搭建用户界面两部分。
创建一个新应用
创建一个新App,勾选Use Core Data 和 Host in CloudKit
\
包含了CoreData和CloudKit的预制模版已经实现了一个最基础的使用CoreData记录数据的App,此时存储的数据已经能够通过CoreData进行持久化存储,但是实机运行可以发现,如果只是关闭App,此前的数据可以如预期被保存下来,但一旦卸载App再次运行,此前的数据就都不见了。其实虽然在创建项目时我们勾选了Host in iCloud,但由于我们并没有配置iCloud相关的内容,因此iCloud并未生效。
配置iCloud
首先,在项目配置的Signing & Capabilites中添加iCloud(注意,需要加入Apple Developer Program这里才会有iCloud可以添加,如果没有的话,可以跳过iCloud配置,app数据将会保存在本地)
勾选iCloud下的CloudKit,添加Container。container的命名需要保证唯一,且创建后无法删除。
如果是选择了一个现有Containers而不是新建,有时在运行应用时会出现Permission Failure,可以在Apple Developer->Certificates,Identifiers&Profiles->Identifiers App IDs,选择对应的BundleID,配置iCloud,点击Edit,重新配置container。
\
注意,我们在添加iCloud的同时,可以看到自动添加了一个Push Notifications,这是因为我们不止需要保存数据,也需要在iCloud上数据发生变更时作出反应,因此需要这样一个消息推送的能力。
为了让应用在后台也能相应云端的数据变更,我们再增加一个Backgroudn Modes,并勾选其中的Remote notifications。
现在,我们已经完成了所有iCloud相关的配置,在登陆了同一个iCloud账号的多个测试机上同时运行App,不同设备间的数据现在终于是同步的了,而且由于数据存储在iCloud,卸载App也不会导致数据的丢失了!
但是,iCloud的数据上传和推送需要一定的时间,一个设备上的数据变更一般需要10-30秒左右才能推送到另一台设备上,如果两台设备同时进行了有冲突的数据操作,此时我们的App就会崩溃报错。我们可以在Persistence中添加下面的代码将冲突合并策略设置为“逐属性比较,如果持久化数据和内存数据都改变且冲突,内存数据胜出”。
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
Weight实体
我们需要在原先的Item实体中添加weight和recordDate字段,并将Item重命名为Weight以便理解
记得将代码中所有用到Item的地方都改为Weight(可以在代码中的Item上右键,Refactor->Rename)
体重记录View
模版中点击➕只记录了点击的时间戳,而我们需要让用户可以选择时间并填写体重,因此还需要一个体重填写弹窗,这里我们使用Sheet从下方弹出表单,表单内包含一个TextFiled输入体重,一个DatePicker输入记录日期。
// AddWeightView.swift
import SwiftUI
struct AddWeightView: View {
@Environment(.managedObjectContext) private var viewContext
@State private var weight = ""
@State private var recordDate = Date()
@Environment(.dismiss) private var dismiss
var body: some View {
NavigationView{
List {
HStack {
Text("体重")
Spacer()
TextField("请输入", text: $weight).multilineTextAlignment(.trailing)
Text("Kg")
}
HStack {
Text("日期")
DatePicker("",selection: $recordDate,displayedComponents: .date)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("取消") {
dismiss()
}
}
ToolbarItem {
Button("记录"){
addItem()
}
}
}
}
}
private func addItem() {
withAnimation {
do {
if let weight = Double(weight) {
let newItem = Weight(context: viewContext)
newItem.recordDate = recordDate
newItem.timestamp = Date()
newItem.weight = weight
try viewContext.save()
dismiss()
}
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error (nsError), (nsError.userInfo)")
}
}
}
}
到这里,我们今天的目标就实现啦!
好课分享:
本文参考:
SwiftUI极简教程20:CoreData数据持久化框架的使用(上)-阿里云开发者社区
东坡肘子:Core Data with CloudKit (一) —— 基础
感谢两位大佬的分享
官方文档:
小知识:
NS前缀是什么?
在IOS开发中,经常会遇到NS开头的对象,这个要从乔帮主历史恩怨说起。当年Steve Jobs 和John Scullery与恩怨,乔帮主当年被人挤兑出苹果,自立门户的时候做了个公司叫做NextStep,里面这一整套开发包很是让一些科学家们喜欢,而现在Mac OS用的就是NextStep这一套函数库。
这些开发NextStep的人们比较自恋地把函数库里面所有的类都用NextStep的缩写打头命名,也就是NS****了。