[Day 001]体重记录-记录数据并保存到iCloud

400 阅读4分钟

本日的文章中,我们将新建一个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)")
            }
        }
    }
}

到这里,我们今天的目标就实现啦!

好课分享:

斯坦福CS193P 2021春季SwiftUI 2.0课程

本文参考:

SwiftUI极简教程20:CoreData数据持久化框架的使用(上)-阿里云开发者社区

东坡肘子:Core Data with CloudKit (一) —— 基础

感谢两位大佬的分享

官方文档:

CloudKit

Core Data Stack

小知识:

NS前缀是什么?

在IOS开发中,经常会遇到NS开头的对象,这个要从乔帮主历史恩怨说起。当年Steve Jobs 和John Scullery与恩怨,乔帮主当年被人挤兑出苹果,自立门户的时候做了个公司叫做NextStep,里面这一整套开发包很是让一些科学家们喜欢,而现在Mac OS用的就是NextStep这一套函数库。
这些开发NextStep的人们比较自恋地把函数库里面所有的类都用NextStep的缩写打头命名,也就是NS****了。