聊一下在SwiftUI中使用CoreData
来源:     阅读:538
织梦模板店
发布于 2021-03-20 05:03
查看主页

本文并非一个教你如何在SwiftUI下使用CoreData的教程。主要讨论的是在我近一年的SwiftUI开发中使用CoreData的教训、经验、心得。

SwiftUI lifecycle 中如何公告持久化存储和上下文

在XCode12中,苹果新添加了SwiftUI lifecycle,让App完全的SwiftUI化。不过这就需要我们使用新的方法来公告持久化存储和上下文。

如同是从beta6开始,XCode 12提供了基于SwiftUI lifecycle的CoreData模板

@mainstruct 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        //根据你的实际需要,创立用于preview的数据        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便将数据保存在内存而非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)")            }        })    }}

尽管对于用于preview的持久化设置并不完美,不过苹果也意识到了在SwiftUI1.0中的一个很大问题,无法preview使用了@FetchRequest的视图。

因为在官方CoreData模板出现前,我已经开始了我的项目构建,因而,我使用了下面的方式来公告

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

但因为我们已经没有办法在SwiftUI lifecycle中如此使用,通过上面的公告我们可以利用下面的方法在全局获取想要的上下文或者其余想要取得的对象

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))    }}//或者者直接操作数据库,都是可以的

如何动态设置 @FetchRequest

在SwiftUI中,假如无需复杂的数据操作,使用CoreData是非常方便的。在完成xcdatamodeld的设置后,我们即可以在View中轻松的操作数据了。

我们通常使用如下语句来获取某个entity的数据

@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.studentId, ascending: true)],              predicate:NSPredicate(format: "age > 10"),              animation: .default) private var students: FetchedResults<Student>

不过如此使用的话,查询条件将无法改变,假如想根据需要调整查询条件,可以使用下面的方法。

健康笔记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内容或者者item名称包含关键字            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)        }    }}

上述代码会根据搜索关键字以及少量其余的范围条件,动态的创立predicate,从而取得所需的数据。

对于相似查询这样的操作,最好配合上Combine来限制数据获取的频次

例如:

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()    }    }

上述所有代码均缺失了很大部分,仅做思路上的说明

添加转换层方便代码开发

在开发健康笔记 1.0的时候我经常被相似下面的代码所烦恼

@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中,设置Attribute,很多时候并不能完全如愿。

好几个类型是可选的,比方String,UUID等,假如在已发布的app,将新添加的attribute其改为不可选,并设置默认值,将极大的添加迁移的难度。另外,假如使用了NSPersistentCloudKitContainer,因为Cloudkit的atrribute和CoreData并不相同,XCode会强制你将很多Attribute改成你不希望的样式。

为了提高开发效率,并为未来的修改留出灵活、充分的更改空间,在健康笔记2.0的开发中,我为每个NSManagedObject都添加了一个便于在View和其余数据操作中使用的中间层。

例如:

@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() //举例   )  }

如此一来,在View中调用将非常方便,同时即便更改entity的设置,整个程序的代码修改量也将明显降低。

ForEach(students){ student in  let student = student.viewModel  Text(student.name)  Text(student.birthdate)}

同时,对于数据的其余操作,我也都通过这个viewModel来完成。

比方:

//MARK:通过ViewModel生成Note数据,所有的prepare动作都需要显示调用 _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数据,仍需显示调用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出现错误")       }       return Just(AppAction.none).eraseToAnyPublisher()    }    func editNote(note:Note,newNoteViewModel:NoteViewModel) -> AnyPublisher<AppAction,Never>{        let _ = _updateNote(note, newNoteViewModel)        if !_coreDataSave() {            logDebug("升级Note出现错误")        }        return Just(AppAction.none).eraseToAnyPublisher()}

在View中调用

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()}

从而将可选值或者者类型转换控制在最小范围

使用NSPersistentCloudKitContainer 需要注意的问题

从iOS13开始,苹果提供了NSPersistentCloudKitContainer,让app可以以最简单的方式享有了数据库云同步功能。

不过在使用中,我们需要注意几个问题。

不要用SQL的思维限制了CoreData的能力

CoreData尽管主要是采用Sqlite来作为数据存储方案,不过对于它的数据对象操作不要完全套用Sql中的惯用思维。

少量例子

排序:

//Sql式的NSSortDescriptor(key: "name", ascending: true)//更CoreData化,不会出现拼写错误NSSortDescriptor(keyPath: \Student.name, ascending: true)

在断言中不适用子查询而直接比较对象:

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        }    }

或者者更加简单的count

@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Student.name, ascending: true)],              animation: .default) private var students: FetchedResults<Student>sutudents.count

对于数据量不大的情况,我们也可以不采用上面的动态predicate方式,在View中直接对获取后的数据进行操作,比方:

@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            }        }   }}

总之数据皆对象

遗憾和不足

苹果在努力提高CoreData在SwiftUI下的体现,不过目前还是有少量遗憾和不足的。

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 软件环境
相关推荐
20届毕业生请看过来!!!
别再被人当棋子耍!这6个“反局”心法,让你活成自己的裁判
Apache MADlib成功晋升为Apache顶级项目!
Linux设置半中文环境(图形界面中文,字符界面英文)
Chrome吃内存的能力可不是说着玩的!
首页
搜索
订单
购物车
我的