本文并非一個(gè)教你如何在SwiftUI下使用CoreData的教程肴沫。主要探討的是在我近一年的SwiftUI開(kāi)發(fā)中使用CoreData的教訓(xùn)略贮、經(jīng)驗(yàn)、心得茫陆。
SwiftUI lifecycle 中如何聲明持久化存儲(chǔ)和上下文
在XCode12中金麸,蘋(píng)果新增了SwiftUI lifecycle,讓App完全的SwiftUI化簿盅。不過(guò)這就需要我們使用新的方法來(lái)聲明持久化存儲(chǔ)和上下文挥下。
好像是從beta6開(kāi)始,XCode 12提供了基于SwiftUI lifecycle的CoreData模板
@main
struct CoreDataTestApp: App {
//持久化聲明
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
//上下文注入
}
}
}
在它的Presitence中桨醋,添加了用于preview的持久化定義
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
//根據(jù)你的實(shí)際需要棚瘟,創(chuàng)建用于preview的數(shù)據(jù)
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
//如果是用于preview便將數(shù)據(jù)保存在內(nèi)存而非sqlite中
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Shared")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
雖然對(duì)于用于preview的持久化設(shè)置并不完美,不過(guò)蘋(píng)果也意識(shí)到了在SwiftUI1.0中的一個(gè)很大問(wèn)題讨盒,無(wú)法preview使用了@FetchRequest的視圖解取。
由于在官方CoreData模板出現(xiàn)前,我已經(jīng)開(kāi)始了我的項(xiàng)目構(gòu)建返顺,因此禀苦,我使用了下面的方式來(lái)聲明
struct HealthNotesApp:App{
static let coreDataStack = CoreDataStack(modelName: "Model") //Model.xcdatemodeld
static let context = DataNoteApp.coreDataStack.managedContext
static var storeRoot = Store()
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
WindowGroup {
rootView()
.environmentObject(store)
.environment(\.managedObjectContext, DataNoteApp.context)
}
}
在UIKit App Delegate中,我們可以使用如下代碼在App任意位置獲取上下文
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
但由于我們已經(jīng)沒(méi)有辦法在SwiftUI lifecycle中如此使用遂鹊,通過(guò)上面的聲明我們可以利用下面的方法在全局獲取想要的上下文或其他想要獲得的對(duì)象
let context = HealthNotesApp.context
比如在 delegate中
class AppDelegate:NSObject,UIApplicationDelegate{
let send = HealthNotesApp.storeRoot.send
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
logDebug("app startup on ios")
send(.loadNote)
return true
}
func applicationDidFinishLaunching(_ application: UIApplication){
logDebug("app quit on ios")
send(.counter(.save))
}
}
//或者直接操作數(shù)據(jù)庫(kù)振乏,都是可以的
如何動(dòng)態(tài)設(shè)置 @FetchRequest
在SwiftUI中,如果無(wú)需復(fù)雜的數(shù)據(jù)操作秉扑,使用CoreData是非常方便的慧邮。在完成xcdatamodeld的設(shè)置后,我們就可以在View中輕松的操作數(shù)據(jù)了舟陆。
我們通常使用如下語(yǔ)句來(lái)獲取某個(gè)entity的數(shù)據(jù)
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.studentId, ascending: true)],
predicate:NSPredicate(format: "age > 10"),
animation: .default)
private var students: FetchedResults<Student>
不過(guò)如此使用的話误澳,查詢(xún)條件將無(wú)法改變,如果想根據(jù)需要調(diào)整查詢(xún)條件秦躯,可以使用下面的方法忆谓。
健康筆記2中的部分代碼:
struct rootView:View{
@State var predicate:NSPredicate? = nil
@State var sort = NSSortDescriptor(key: "date", ascending: false)
@StateObject var searchStore = SearchStore()
@EnvironmentObject var store:Store
var body:some View{
VStack {
SearchBar(text: $searchStore.searchText) //搜索框
MemoList(predicate: predicate, sort: sort,searching:searchStore.showSearch)
}
.onChange(of: searchStore.text){ _ in
getMemos()
}
}
//讀取指定范圍的memo
func getMemos() {
var predicators:[NSPredicate] = []
if !searchStore.searchText.isEmpty && searchStore.showSearch {
//memo內(nèi)容或者item名稱(chēng)包含關(guān)鍵字
predicators.append(NSPredicate(format: "itemData.item.name contains[cd] %@ OR content contains[cd] %@", searchStore.searchText,searchStore.searchText))
}
if star {
predicators.append(NSPredicate(format: "star = true"))
}
switch store.state.memo{
case .all:
break
case .memo:
if !searchStore.searchText.isEmpty && noteOption == 1 {
break
}
else {
predicators.append(NSPredicate(format: "itemData.item.note = nil"))
}
case .note(let note):
if !searchStore.searchText.isEmpty && noteOption == 1 {
break
}
else {
predicators.append(NSPredicate(format: "itemData.item.note = %@", note))
}
}
withAnimation(.easeInOut){
predicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.and, subpredicates: predicators)
sort = NSSortDescriptor(key: "date", ascending: ascending)
}
}
}
上述代碼會(huì)根據(jù)搜索關(guān)鍵字以及一些其他的范圍條件,動(dòng)態(tài)的創(chuàng)建predicate踱承,從而獲得所需的數(shù)據(jù)倡缠。
對(duì)于類(lèi)似查詢(xún)這樣的操作,最好配合上Combine來(lái)限制數(shù)據(jù)獲取的頻次
例如:
class SearchStore:ObservableObject{
@Published var searchText = ""
@Published var text = ""
@Published var showSearch = false
private var cancellables:[AnyCancellable] = []
func registerPublisher(){
$searchText
.removeDuplicates()
.debounce(for: 0.4, scheduler: DispatchQueue.main)
.assign(to: &$text)
}
func removePublisher(){
cancellables.removeAll()
}
}
上述所有代碼均缺失了很大部分茎活,僅做思路上的說(shuō)明
增加轉(zhuǎn)換層方便代碼開(kāi)發(fā)
在開(kāi)發(fā)健康筆記 1.0的時(shí)候我經(jīng)常被類(lèi)似下面的代碼所煩惱
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],
animation: .default)
private var students: FetchedResults<Student>
ForEach(students){ student in
Text(student.name ?? "")
Text(String(student.date ?? Date()))
}
在CoreData中昙沦,設(shè)置Attribute,很多時(shí)候并不能完全如愿载荔。
好幾個(gè)類(lèi)型是可選的盾饮,比如String,UUID等,如果在已發(fā)布的app丐谋,將新增的attribute其改為不可選芍碧,并設(shè)置默認(rèn)值煌珊,將極大的增加遷移的難度号俐。另外,如果使用了NSPersistentCloudKitContainer,由于Cloudkit的atrribute和CoreData并不相同定庵,XCode會(huì)強(qiáng)制你將很多Attribute改成你不希望的樣式吏饿。
為了提高開(kāi)發(fā)效率,并為未來(lái)的修改留出靈活蔬浙、充分的更改空間猪落,在健康筆記2.0的開(kāi)發(fā)中,我為每個(gè)NSManagedObject都增加了一個(gè)便于在View和其他數(shù)據(jù)操作中使用的中間層畴博。
例如:
@objc(Student)
public class Student: NSManagedObject,Identifiable {
@NSManaged public var name: String?
@NSmanaged public var birthdate: Date?
}
public struct StudentViewModel: Identifiable{
let name:String
let birthdate:String
}
extension Student{
var viewModel:StudentViewModel(
name:name ?? ""
birthdate:(birthdate ?? Date()).toString() //舉例
)
}
如此一來(lái)笨忌,在View中調(diào)用將非常方便,同時(shí)即使更改entity的設(shè)置俱病,整個(gè)程序的代碼修改量也將顯著降低官疲。
ForEach(students){ student in
let student = student.viewModel
Text(student.name)
Text(student.birthdate)
}
同時(shí),對(duì)于數(shù)據(jù)的其他操作亮隙,我也都通過(guò)這個(gè)viewModel來(lái)完成途凫。
比如:
//MARK:通過(guò)ViewModel生成Note數(shù)據(jù),所有的prepare動(dòng)作都需要顯示調(diào)用 _coreDataSave()
func _prepareNote(_ viewModel:NoteViewModel) -> Note{
let note = Note(context: context )
note.id = viewModel.id
note.index = Int32(viewModel.index)
note.createDate = viewModel.createDate
note.name = viewModel.name
note.source = Int32(viewModel.source)
note.descriptionContent = viewModel.descriptionContent
note.color = viewModel.color.rawValue
return note
}
//MARK:更新Note數(shù)據(jù),仍需顯示調(diào)用save
func _updateNote(_ note:Note,_ viewModel:NoteViewModel) -> Note {
note.name = viewModel.name
note.source = Int32(viewModel.source)
note.descriptionContent = viewModel.descriptionContent
note.color = viewModel.color.rawValue
return note
}
func newNote(noteViewModel:NoteViewModel) -> AnyPublisher<AppAction,Never> {
let _ = _prepareNote(noteViewModel)
if !_coreDataSave() {
logDebug("新建Note出現(xiàn)錯(cuò)誤")
}
return Just(AppAction.none).eraseToAnyPublisher()
}
func editNote(note:Note,newNoteViewModel:NoteViewModel) -> AnyPublisher<AppAction,Never>{
let _ = _updateNote(note, newNoteViewModel)
if !_coreDataSave() {
logDebug("更新Note出現(xiàn)錯(cuò)誤")
}
return Just(AppAction.none).eraseToAnyPublisher()
}
在View中調(diào)用
Button("New"){
let noteViewModel = NoteViewModel(createDate: Date(), descriptionContent: myState.noteDescription, id: UUID(), index: -1, name: myState.noteName, source: 0, color: .none)
store.send(.newNote(noteViewModel: noteViewModel))
presentationMode.wrappedValue.dismiss()
}
從而將可選值或者類(lèi)型轉(zhuǎn)換控制在最小范圍
使用NSPersistentCloudKitContainer 需要注意的問(wèn)題
從iOS13開(kāi)始,蘋(píng)果提供了NSPersistentCloudKitContainer溢吻,讓app可以以最簡(jiǎn)單的方式享有了數(shù)據(jù)庫(kù)云同步功能维费。
不過(guò)在使用中,我們需要注意幾個(gè)問(wèn)題促王。
Attribute
在上一節(jié)提高過(guò)犀盟,由于Cloudkit的數(shù)據(jù)設(shè)定和CoreData并不完全兼容,因此如果你在項(xiàng)目初始階段是使用NSPersistentContainer進(jìn)行開(kāi)發(fā)的蝇狼,當(dāng)將代碼改成NSPersistentCloudKitContainer后阅畴,XCode可能會(huì)提示你某些Attribute不兼容的情況。如果你采用了中間層處理數(shù)據(jù)题翰,修改起來(lái)會(huì)很方便恶阴,否則你需要對(duì)已完成的代碼做出不少的修改和調(diào)整。我通常為了開(kāi)發(fā)調(diào)試的效率豹障,只有到最后的時(shí)候才會(huì)使用NSPersistentCloudKitContainer冯事,因此這個(gè)問(wèn)題會(huì)比較突出。-
合并策略
奇怪的是血公,在XCode的CoreData(點(diǎn)選使用CloudKit)默認(rèn)模板中昵仅,并沒(méi)有設(shè)定合并策略。如果沒(méi)有設(shè)置的話,當(dāng)app的數(shù)據(jù)進(jìn)行云同步時(shí)摔笤,時(shí)長(zhǎng)會(huì)出現(xiàn)合并錯(cuò)誤够滑,并且@FetchRequest也并不會(huì)在有數(shù)據(jù)發(fā)生變動(dòng)時(shí)對(duì)View進(jìn)行刷新。因此我們需要自己明確數(shù)據(jù)的合并策略吕世。lazy var persistentContainer: NSPersistentCloudKitContainer = { let container = NSPersistentCloudKitContainer(name: modelName) container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) //需要顯式表明下面的合并策略,否則會(huì)出現(xiàn)合并錯(cuò)誤! container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return container }()
-
調(diào)試信息
當(dāng)打開(kāi)云同步后彰触,在調(diào)試信息中將出現(xiàn)大量的數(shù)據(jù)同步調(diào)試信息,嚴(yán)重影響了對(duì)于其他調(diào)試信息的觀察命辖。雖然可以通過(guò)啟動(dòng)命令屏蔽掉數(shù)據(jù)同步信息况毅,但有時(shí)候我還是需要對(duì)其進(jìn)行觀察的。目前我使用了一個(gè)臨時(shí)的解決方案尔艇。#if !targetEnvironment(macCatalyst) && canImport(OSLog) import OSLog let logger = Logger.init(subsystem: "com.fatbobman.DataNote", category: "main") //調(diào)試用 func logDebug(_ text:String,enable:Bool = true){ #if DEBUG if enable { logger.debug("\(text)") } #endif } #else func logDebug(_ text:String,enable:Bool = true){ print(text,"$$$$") } #endif
對(duì)于需要顯示調(diào)試信息的地方
logDebug("數(shù)據(jù)格式錯(cuò)誤")
然后通過(guò)在Debug窗口中將Filter設(shè)置為$$$$來(lái)屏蔽掉暫時(shí)不想看到的其他信息
不要用SQL的思維限制了CoreData的能力
CoreData雖然主要是采用Sqlite來(lái)作為數(shù)據(jù)存儲(chǔ)方案尔许,不過(guò)對(duì)于它的數(shù)據(jù)對(duì)象操作不要完全套用Sql中的慣用思維。
一些例子
排序:
//Sql式的
NSSortDescriptor(key: "name", ascending: true)
//更CoreData化终娃,不會(huì)出現(xiàn)拼寫(xiě)錯(cuò)誤
NSSortDescriptor(keyPath: \Student.name, ascending: true)
在斷言中不適用子查詢(xún)而直接比較對(duì)象:
NSPredicate(format: "itemData.item.name = %@",name)
Count:
func _getCount(entity:String,predicate:NSPredicate?) -> Int{
let fetchRequest = NSFetchRequest<NSNumber>(entityName: entity)
fetchRequest.predicate = predicate
fetchRequest.resultType = .countResultType
do {
let results = try context.fetch(fetchRequest)
let count = results.first!.intValue
return count
}
catch {
#if DEBUG
logDebug("\(error.localizedDescription)")
#endif
return 0
}
}
或者更加簡(jiǎn)單的count
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],
animation: .default)
private var students: FetchedResults<Student>
sutudents.count
對(duì)于數(shù)據(jù)量不大的情況味廊,我們也可以不采用上面的動(dòng)態(tài)predicate方式,在View中直接對(duì)獲取后的數(shù)據(jù)進(jìn)行操作棠耕,比如:
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],
animation: .default)
private var studentDatas: FetchedResults<Student>
@State var students:[Student] = []
var body:some View{
List{
ForEach(students){ student in
Text(student.viewModel.name)
}
}
.onReceive(studentDatas.publisher){ _ in
students = studentDatas.filter{ student in
student.viewModel.age > 10
}
}
}
}
總之?dāng)?shù)據(jù)皆對(duì)象
遺憾和不足
蘋(píng)果在努力提高CoreData在SwiftUI下的表現(xiàn)余佛,不過(guò)目前還是有一些遺憾和不足的。
- @FetchRequest的控制選項(xiàng)太少
當(dāng)前我們無(wú)法設(shè)置FetchRequest的limitNumber以及returnsObjectsAsFaults昧辽,它會(huì)直接將所有的數(shù)據(jù)讀入到上下文中衙熔,當(dāng)數(shù)據(jù)量較大時(shí),這樣的效率是很低下的搅荞。所以如果需要處理較大數(shù)據(jù)集的時(shí)候红氯,最好不要依賴(lài)@FetchRequest。 - animation有些神經(jīng)刀
在List中顯示@FetchRquest獲取的數(shù)據(jù)集咕痛,即使你明確設(shè)置了animation(FetchRequest痢甘,以及List),并且也顯式的使用了withAnimation對(duì)所需操作強(qiáng)制動(dòng)畫(huà)調(diào)用,但動(dòng)畫(huà)并不能總?cè)缒愕念A(yù)期般實(shí)現(xiàn)茉贡。完全相同的代碼塞栅,放置在不同的地方,有時(shí)會(huì)出現(xiàn)不同的結(jié)果腔丧。
當(dāng)通過(guò)UITableViewDiffableDataSource數(shù)據(jù)來(lái)調(diào)用自己包裝的UITableView后放椰,動(dòng)畫(huà)就不會(huì)再不可控了。希望蘋(píng)果能早點(diǎn)解決這個(gè)Bug.