I am trying to create a simple application that leverages CloudKit to sync between multiple iOS devices (via iCloud) and possibly macOS devices (still via iCloud).
Problem:
I have a core data entity which seems to work great locally in my app. When I switch to using Cloudkit I am unable to see changes on another device without closing/reopening the app.
I am using the Cloudkit template in Xcode with the SwiftUI lifecycle. ie, PersistenceController and managed object context.
I think this is a view not refreshing issue, but am not 100% sure. Once the app on a different device is closed and reopened then it loads the new data successfully. This applies to additions and deletes.
Testing:
I have tested this using Testflight as well as running two simulators locally.
Code:
PersistenceController.swift
import CoreData
struct PersistenceController {
static var shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for i in 0..<10 {
let newItem = Card(context: viewContext)
newItem.cardValue = String(i)
newItem.cardOrder = Int32(i)
}
do {
try viewContext.save()
} 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)")
}
return result
}()
//Is this needed?
lazy var updateContext: NSManagedObjectContext = {
let _updateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
_updateContext.parent = PersistenceController.shared.updateContext
return _updateContext
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Cards")
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)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
}
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(entity: Card.entity(),
sortDescriptors: [NSSortDescriptor(key: "cardOrder", ascending: true)])
var cards: FetchedResults<Card>
var body: some View {
NavigationView {
VStack {
Text("Scrum +")
.font(.largeTitle)
.fontWeight(.regular)
Spacer()
List(cards, id: \.id) {card in
NavigationLink(destination: PointRow(card: card)) {
PointRow(card: card)
}
}
Spacer()
NavigationLink(destination: SettingsView().environment(\.managedObjectContext, self.managedObjectContext)
) {
Text("Card Settings")
}
.padding(.bottom)
}
}
}
}
SettingsView.swift (where saving/deleting/reordering happens)
import SwiftUI
struct SettingsView: View {
#State private var editMode = EditMode.inactive
#State private var showModal = false
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(key: "cardOrder", ascending: true)])
var cards: FetchedResults<Card>
var body: some View {
VStack {
Text("Card Values")
.font(.largeTitle)
HStack {
EditButton()
.padding(.leading)
Spacer()
addButton
.padding(.trailing)
}
.padding(.vertical)
List {
ForEach(cards) {
item in
Text(item.cardValue!)
}
.onDelete(perform: onDelete)
.onMove(perform: onMove)
.environment(\.editMode, $editMode)
}
.sheet(isPresented: $showModal) {
//Show the view to add a new card.
SettingsModal(showModal: self.$showModal, numberOfCards: cards.count).environment(\.managedObjectContext, self.managedObjectContext)
}
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
func onAdd() {
showModal = true
}
func onDelete(indexSet: IndexSet) {
// print("Deleting card at index -> " + indexSet.first)
let cardToDelete = self.cards[indexSet.first!]
self.managedObjectContext.delete(cardToDelete)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
reorder()
}
private func onMove(source: IndexSet, destination: Int) {
// pointCards.points.move(fromOffsets: source, toOffset: destination)
// source.
let firstCard = self.cards[source.first!]
//If there is a card located in the destination.
if (destination < self.cards.count) {
let secondCard = self.cards[destination]
let tmp = Int(secondCard.cardOrder)
//Increment all place holders from the destination on.
for i in tmp..<self.cards.count {
self.cards[i].cardOrder += 1
}
}
firstCard.cardOrder = Int32(destination)
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
self.managedObjectContext.refreshAllObjects()
//Reorder just in case moved to the end.
reorder()
}
private func reorder() {
for i in 0..<cards.count {
cards[i].cardOrder = Int32(i)
}
do {
try self.managedObjectContext.save()
} catch {
print(error)
}
}
}
Screenshots of my Core Data model using Cloudkit and of the Entitlement selection.
Capabilities
CoreData Properties
UPDATE:
I have found the solution.
The Persistent Controller should have the following three lines added to the init function:
//Setup auto merge of Cloudkit data
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
//Set the Query generation to .current. for dynamically updating views from Cloudkit
try? container.viewContext.setQueryGenerationFrom(.current)
Then this must be ran on real devices. For whatever reason I am unable to get this working in the simulator, but on real devices it syncs in about 30 seconds without closing the app.
Thanks everyone for your help!
Related
I'm using CoreData + CloudKit in my app, and my views aren't reflecting changes in CloudKit. For example, when I delete records on the web CloudKit dashboard, they still show in my view in the simulator. Running the app again from Xcode still reflects old data in the simulator.
I was able to recreate this problem using the default Xcode project with CloudKit and CoreData boxes checked and minimal additions to Persistence.swift.
Here's that code
Persistence.swift
import CloudKit
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} 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)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "TestProject")
let containerIdentifier = "[MY CONTAINER IDENTIFIER]"
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
// Default store
let defaultStoreLocation = container.persistentStoreDescriptions.first!.url!
// Setup public store
let publicStoreLocation = defaultStoreLocation.deletingLastPathComponent().appendingPathComponent("public.sqlite")
let publicStoreDescription = NSPersistentStoreDescription(url: publicStoreLocation)
publicStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: containerIdentifier)
publicStoreDescription.cloudKitContainerOptions?.databaseScope = .public
// Load persistent store descriptions
container.persistentStoreDescriptions = [publicStoreDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// 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.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
And here's the default ContentView
ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} 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)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} 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)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
Here's the default App
TestProjectApp.swift
import SwiftUI
#main
struct TestProjectApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
Have a look at this https://developer.apple.com/videos/play/wwdc2020/10650/ at around 14:00 on deleting items in the public database.
You would have to set a flag isTrashed for example and then filter items based on that instead of deleting them for the Public database.
I posted this question:
SwiftUI: deleting Managed Object from cell view crashes app?
as I worked on trying to understand why it crashes, I tried to change the model Item to have timestamp NON-optional:
extension Item {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
return NSFetchRequest<Item>(entityName: "Item")
}
#NSManaged public var timestamp: Date
}
extension Item : Identifiable {
}
As Asperi pointed out, using this:
if let timestamp = item.timestamp {
Text(timestamp, formatter: itemFormatter)
}
does fix the crash when timestamp is optional.
However, this is just some code I am testing to understand how to properly build my views. I need to use models that do not have optional properties, and because of that I can't resort to use the provided answer to the question I linked to above.
So this question is to address the scenario where my CellView uses a property that is not optional on a ManagedObject.
If I were to put this code straight in the ContentView without using the CellView it does not crash. This does not crash:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text(item.timestamp, formatter: itemFormatter)
} label: {
// CellView(item: item)
HStack {
Text(item.timestamp, formatter: itemFormatter) // <<- CRASH ON DELETE
Button {
withAnimation {
viewContext.delete(item)
try? viewContext.save()
}
} label: {
Text("DELETE")
.foregroundColor(.red)
}
.buttonStyle(.borderless)
}
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach { item in
viewContext.delete(item)
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
However, I need to know how to keep the CellView, use #ObservedObject and make this work. In this case is not a big deal to do that, but in real cases where the CellView is much bigger this approach does not scale well. Regardless, why would using #ObservedObject in a separate view be wrong anyway?
So, why is the app crashing when the timestamp is NOT optional in the model?
Why is the view trying to redraw the CellView for an Item that was deleted? How can this be fixed?
FOR CLARITY I AM POSTING HERE THE NEW CODE FOR THE NON-OPTIONAL CASE, SO YOU DON'T HAVE TO GO BACK AND LOOK AT THE LINKED QUESTION AND THEN CHANGE IT TO NON-OPTIONAL. THIS IS THE CODE THAT CRASHES IN ITS ENTIRETY:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
Text(item.timestamp, formatter: itemFormatter)
} label: {
CellView(item: item)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach { item in
viewContext.delete(item)
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
struct CellView: View {
#Environment(\.managedObjectContext) private var viewContext
#ObservedObject var item:Item
var body: some View {
HStack {
Text(item.timestamp, formatter: itemFormatter) // <<- CRASH ON DELETE
Button {
withAnimation {
viewContext.delete(item)
try? viewContext.save()
}
} label: {
Text("DELETE")
.foregroundColor(.red)
}
.buttonStyle(.borderless)
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
The explicit handling is needed in anyway, because of specifics of CoreData engine. After object delete it can still be in memory (due to kept references), but it becomes in Fault state, that's why code autogeneration always set NSManagedObject properties to optional (even if they are not optional in model).
Here is a fix for this specific case. Tested with Xcode 13.4 / iOS 15.5
if !item.isFault {
Text(item.timestamp, formatter: itemFormatter) // << NO CRASH
}
I have a SwiftUI + CoreData simple Todo app, and everything works properly, but my updateTodo function which is supposed to handle the click on a todo and turn in from done to undone and vice versa, isn't working properly.
When I click on a todo nothing happens in the UI, but when I go a screen back and come back to the todos screen, I can see the UI change, also it does persist so when I close and open the app the change is being reflected in the app.
So my problem is that the 'isDone' property is not being toggled in the UI in real-time, and only when the view reappears it actually shows the change that has been made.
ViewModel (CoreData) :
class TodoViewModel:ObservableObject {
let container: NSPersistentContainer
#Published var categories = [CategoryTodo]()
#Published var todos = [Todo]()
init() {
container = NSPersistentContainer(name: "UniversityProject")
container.loadPersistentStores { (description, error) in
if let error = error {
fatalError("Error: \(error.localizedDescription)")
}
}
}
//MARK: - Todos
func getTodos() {
let request = NSFetchRequest<Todo>(entityName: "Todo")
let sort = NSSortDescriptor(key: #keyPath(Todo.dateCreated), ascending: false)
request.sortDescriptors = [sort]
do {
try todos = container.viewContext.fetch(request)
} catch {
print("Error getting data. \(error)")
}
}
func addTodo(todo text: String, category categoryName:String) {
let newTodo = Todo(context: container.viewContext)
newTodo.todo = text
newTodo.category = categoryName
newTodo.id = UUID().uuidString
newTodo.isDone = false
newTodo.dateCreated = Date()
saveTodo()
}
func saveTodo() {
do {
try container.viewContext.save()
getTodos()
} catch let error {
print("Error: \(error)")
}
}
func deleteTodo(indexSet: IndexSet) {
let todoIndex = indexSet[indexSet.startIndex]
let object = todos[todoIndex]
container.viewContext.delete(object)
saveTodo()
}
func updateTodo(item: Todo) {
item.setValue(!item.isDone, forKey: "isDone")
saveTodo()
}
}
TodosView:
struct TodosView: View {
#EnvironmentObject var viewModel: TodoViewModel
let categoryName:String
var body: some View {
List {
ForEach(viewModel.todos.filter{$0.category == categoryName}) { item in
TodoItem(item: item)
.onTapGesture {
withAnimation(.linear) {
viewModel.updateTodo(item: item)
}
}
}
.onDelete(perform: viewModel.deleteTodo)
}.onAppear { viewModel.getTodos() }
.navigationBarTitle(categoryName)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack {
EditButton()
NavigationLink(destination: AddTodoView(category: categoryName)) {
Image(systemName: "plus.circle")
.resizable()
.foregroundColor(.blue)
.frame(width: 25, height: 25)
}
}
}
}
}
}
without all relevent code, I can only guess and suggest you try something like these:
func updateTodo(item: Todo) {
if let ndx = todos.firstIndex(where: {$0.id == item.id}) {
todos[ndx].isDone = !item.isDone
saveTodo()
}
}
or
func updateTodo(item: Todo) {
objectWillChange.send()
item.setValue(!item.isDone, forKey: "isDone")
saveTodo()
}
I'm having some confusion with CoreData using SwiftUI, I'm just doing an example project for a Toggle. I set the newly created item in PersistenceController but the toggle always remains off. I don't have much experience using CoreData, my previous question I about CoreData was incorrect and I was misinformed about a fix.
PersistenceController
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.bool = false
}
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
ContentView
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Item.bool, ascending: true)], animation: .default)
private var items: FetchedResults<Item>
var body: some View {
VStack {
Toggle("", isOn: Binding<Bool>(
get: { self.items.first?.bool ?? false },
set: {
self.items.first?.bool = $0
try? viewContext.save()
}
))
.labelsHidden()
.frame(width: 100, height: 100, alignment: .center)
}
}
}
As it goes from the name of variable, preview is only used in preview, at least in newly generated project. So on real run your database if empty
Using sortDescriptors your list of items is sorted ascending by bool. This means that as soon as you set first item bool to true, it gets moved to the end of the list, and you're reading bool value from an other object
To create a new Item at start when your database is empty, you need to wait persistent store load, fetch items and check that there're no items, in this case - create a new one. Replace PersistenceController init with following:
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "SwiftUICodeDataPlayground")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { [self] (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
if (try? container.viewContext.fetch(Item.fetchRequest()).isEmpty) != false {
let newItem = Item(context: container.viewContext)
newItem.bool = false
}
})
}
I have a SwiftUI app with full SwiftUI flow with Core Data persistence. It is a basic
master/detail style and it solidly works as expected. I want to add a feature to allow
multiple rows to be selected and deleted. Single row swipe to delete works as expected.
I had inspiration from this SO: 58910010 however, that example used an array of
struct items and I have not been able to duplicate the procedure with Core Data objects.
Basically, the functionality of presenting edit check boxes and the button toggling
functions work as expected, but when I tap the Trash icon, the app crashes. There is
no information in the console and nothing of help to me in the thread log.
See Note 1 below in deleteCards(). If I comment out the viewContext save, the app does
not crash and the selected cards are removed from the list but on restart, the cards
still exist. If I leave the viewContext save in place, the app crashes, but on
restart I see that the selected items no longer exist.
Here's a simplified version:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Card.lastName, ascending: true)],
animation: .default)
private var cards: FetchedResults<Card>
#State var editMode: EditMode = .inactive
#State var selection = Set<UUID>()
var body: some View {
NavigationView {
List(selection: $selection) {
ForEach(cards) { card in
VStack(alignment: .leading) {
Text("\(card.wrappedFirstName) \(card.wrappedLastName)")
Text("\(card.wrappedComment)")
}
}
.onDelete(perform: deleteItems)
}
.navigationBarItems(
leading: editButton,
trailing:
HStack {
addDelButton
addExamplesButton
}
)
.environment(\.editMode, self.$editMode)
.navigationTitle("Cards")
}//nav
}//body
private var editButton: some View {
Button(action: {
self.editMode.toggle()
self.selection = Set<UUID>()
}) {
Text(self.editMode.title)
}
}//edit button
private var addDelButton: some View {
if editMode == .inactive {
return Button(action: addCard) {
Image(systemName: "plus")
}
} else {
return Button(action: deleteCards) {
Image(systemName: "trash")
}
}
}//add del button
private var addExamplesButton: some View {
Button(action: {
self.addExampleRecords()
}) {
Image(systemName: "gear")
}
}//add examples
private func deleteCards() {
for id in selection {
if let index = cards.lastIndex(where: { $0.id == id }) {
viewContext.delete(cards[index])
}
}
selection = Set<UUID>()
//Note 1: this crashed the app - but deletions happen
do {
try viewContext.save()
} catch {
// Replace this
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}//delete Cards
private func addCard() {
withAnimation {
let newCard = Card(context: viewContext)
newCard.id = UUID()
newCard.firstName = "NewFirstName"
newCard.lastName = "NewLastName"
newCard.creationDate = Date()
do {
try viewContext.save()
} catch {
// Replace this
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}//add card
private func addExampleRecords() {
let context = self.viewContext
for c in 0..<10 {
let nmo = Card(context: context)
nmo.id = UUID()
nmo.lastName = "ExLast \(c)"
nmo.firstName = "ExFirst \(c)"
nmo.comment = "ExComment \(c)"
nmo.creationDate = Date()
}
}//add example records
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { cards[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}//delete items
}//struct
extension EditMode {
var title: String {
self == .active ? "Done" : "Edit"
}
mutating func toggle() {
self = self == .active ? .inactive : .active
}
}// ext
And for completeness:
extension Card {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
return NSFetchRequest<Card>(entityName: "Card")
}
#NSManaged public var id: UUID
#NSManaged public var firstName: String?
#NSManaged public var comment: String?
#NSManaged public var lastName: String?
#NSManaged public var creationDate: Date?
public var wrappedFirstName: String {
firstName ?? "no first name"
}
public var wrappedLastName: String {
lastName ?? "no last name"
}
public var wrappedComment: String {
comment ?? "no comment"
}
public var wrappedCreationDate: Date {
creationDate ?? Date()
}
}
extension Card : Identifiable {
}
Any guidance would be appreciated. Xcode 12.3 iOS 14.2