I am upgrading my app to iOS 10 / swift 3
So constructing an NSManagedObject with a dictionary (values received from remote db) this way...
let writerDictionary = [
...: writer.id,
...: writer.name,
...: writer.picture,
...: writer.publicationID,
...: writer.language,
...: writer.country
]
let newWriter = Writer(dictionary: writerDictionary, context: SHARED_CONTEXT)
isFavorite is an NSManaged property that I set manually to true with newWriter.isFavorite = true right after constructing the newWriter object before saving the context -
This line crashes with Uncaught Exception in iOS 10 - Previously there was no problem whatsoever with iOS 9
I have also tried newWriter.setValue(true, forKey: "isFavorite") - While it does not crash the app, the bool value isFavorite remains false unchanged.
Any ideas? This is my Writer subclass of NSManagedObject
import UIKit
import CoreData
#objc(Writer)
class Writer: NSManagedObject {
// Attributes also in the database
#NSManaged var id: NSNumber
#NSManaged var name: String
#NSManaged var picture: String?
#NSManaged var publicationID: NSNumber
#NSManaged var language: String
#NSManaged var country: String
#NSManaged var lastArticle: Date
// Attributes only in the iOS app
#NSManaged var isFavorite: Bool // Initial value is false
#NSManaged var hasNewArticles: Bool
// Relationship objects
#NSManaged var publication: Publication
#NSManaged var articles: [Article]
var writerImage: UIImage? {
get {
return ImageCache.sharedCache.imageWithIdentifier("writers-" + String(id))
}
set {
ImageCache.sharedCache.storeImage(newValue, withIdentifier: "writers-" + String(id))
}
}
override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: context)
}
init(dictionary: [String : AnyObject], context: NSManagedObjectContext) {
let entity = NSEntityDescription.entity(forEntityName: "Writer", in: context)!
super.init(entity: entity, insertInto: context)
id = ...
name = ...
picture = ...
publicationID = ...
language = ...
country = ...
lastArticle = ...
}
override func prepareForDeletion() {
if let _ = writerImage {
writerImage = nil
}
}
}
Solution
The current version of Swift 3 beta seems to have some flaw about treating properties prefixed with "is".
Adjusting the name of the property like this, solved it...
#NSManaged #objc(isFavorite) var isFavorite: Bool
Fixed in Xcode 8 beta 3 compiler.
From the release notes:
Earlier Swift 3 betas implicitly renamed the property, getter, and
setter for #objc properties of type Bool whose names started with
“is”, to match the Objective-C naming conventions. (For example, a
Swift boolean property named isMagical generated an Objective-C
declaration of the form #property (nonatomic, getter=isMagical) BOOL
magical.) This turned out to cause more trouble than it was worth and
has been removed. (26847223)
Related
I make a list for audio items from coredata. after deleting, crash reported as "EXC_BREAKPOINT (code=1, subcode=0x1b8fb693c)", why?
When using
ForEach(items, id: \.self)
, it works. But My Audio has id property and follow Identifiable protocol.
UPDATE: I found adding a if{} clause will fix crash, but why? Breakpoint at "static UUID.unconditionallyBridgeFromObjectiveC(:) ()".
struct Test1View: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(fetchRequest: Audio.fetchAllAudios()) var items: FetchedResults<Audio>
var body: some View {
List {
ForEach(items) { item in
if true { // <- this if clause fix crash, but why?
HStack {
Text("\(item.name)")
}
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
code as following:
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
struct Test1View: View {
#Environment(\.managedObjectContext) var context
var fetchRequest: FetchRequest<Audio> = FetchRequest<Audio>(entity: Audio.entity(), sortDescriptors: [NSSortDescriptor(key: "createAt", ascending: false)])
var items: FetchedResults<Audio> { fetchRequest.wrappedValue }
var body: some View {
List {
ForEach(items) { item in
HStack {
Text("\(item.name)")
}
}.onDelete(perform: { indexSet in
let index = indexSet.first!
let item = self.items[index]
self.context.delete(item)
try? self.context.save()
})
}
}
}
I had the same problem as you.
Probably it is a bad idea to use your own id property to make it Identifiable because Core Data is setting all the properties to nil when the object is deleted, even if you declared it differently in your Swift CoreData class.
When deleting your entity, the id property gets invalidated and the objects .isFault property is set to true, but the SwiftUI ForEach still holds some reference to this ID object (=your UUID) to be able to calculate the "before" and "after" state of the list and somehow tries to access it, leading to the crash.
Therefore the following recommendations:
Protect the detail view (in the ForEach loop by checking isFault:
if entity.isFault {
EmptyView()
}
else {
// your regular view body
}
Expect your id property to be nil, either by defining it accordingly in your core data model as optional
#NSManaged public var id: UUID?
or by not relying on the Identifiable protocol in the SwiftUI ForEach loop:
ForEach(entities, id: \.self) { entity in ... }
or
ForEach(entities, id: \.objectID) { entity in ... }
Conclusion: you really do not need to make all your CoreData properties Swift Optionals. It's simply important that your id property referenced in the ForEach loop handles the deletion (=setting its value to nil) gracefully.
I found the reason of crash, must provide optional, because of OC/swift object conversion:
convert
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID
#NSManaged public var name: String
#NSManaged public var createAt: Date
}
to
class Audio: NSManagedObject, Identifiable {
#NSManaged public var id: UUID?
#NSManaged public var name: String?
#NSManaged public var createAt: Date?
}
I had the same issue over the weekend. It looks like SwiftUI want's to unwrap the value i read from CoreData and as the value is already deleted it crashes.
In my case i did solve it with nil coalescing on all values i use from CoreData.
You can try to provide a default value on your item.name with
ForEach(items) { item in
HStack {
Text("\(item.name ?? "")")
}
}
I am trying to have a simple ForEach within a List (and have done this in another Project), but in this project I am getting the below mentioned error? How could I fix?
Error
Cannot invoke initializer for type 'List' with an argument list of type '(#escaping () -> ForEach, Int64, Text>)'
Code
import SwiftUI
struct ContentView: View {
#FetchRequest(fetchRequest: GCList.allListFetchRequest()) var gcLists: FetchedResults<GCList>
var body : some View {
NavigationView {
List { // <=== ERROR IS MARKED HERE IN XCODE
ForEach(self.gcLists) { gcList in
Text(gcList.title)
}
}
}
}
}
and
extension GCList : Identifiable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<GCList> {
return NSFetchRequest<GCList>(entityName: "GCList")
}
#NSManaged public var id: Int64
#NSManaged public var title: String
}
extension GCList {
static func allListFetchRequest() -> NSFetchRequest<GCList> {
let request: NSFetchRequest<GCList> = GCList.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)]
return request
}
}
Turns out in Core Data I had an old entity that I had created called "List" that managed to much things up....
The problem I am trying to solve is the following: a predicate of an NSFetchedResultsController is restricting the results to specific conditions. But when data objects either newly satisfy or dissatisfy conditions, the NSFetchedResultsControllerDelegate is not getting called.
My NSFetchedResultsController:
private var _fetchedResultsController : NSFetchedResultsController<CardIndex>? = nil
var fetchedResultController : NSFetchedResultsController<CardIndex> {
get {
if _fetchedResultsController == nil {
let fetchRequest: NSFetchRequest<CardIndex> = CardIndex.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor.init(key: "index", ascending: true)]
fetchRequest.predicate = NSPredicate.init(format: "consumer == %# AND card.definition != nil AND card.stateInt == 0", consumer)
_fetchedResultsController = NSFetchedResultsController<CardIndex>.init(fetchRequest: fetchRequest, managedObjectContext: CDManager.shared.one.viewContext, sectionNameKeyPath: nil, cacheName: nil)
do {
try _fetchedResultsController?.performFetch()
} catch {
Log(.Warning, "...")
}
_fetchedResultsController?.delegate = self
}
return _fetchedResultsController!
}
}
The data structure that is relevant here (only pseudo code):
class CardIndex {
#NSManaged public var index: Int16
#NSManaged public var consumer: String?
#NSManaged public var card: CardApplication?
}
class CardApplication {
#NSManaged public var title: String?
#NSManaged public var indexes: NSSet?
#NSManaged public var stateInt: NSNumber?
#NSManaged public var definition: CardDefinition?
}
When the NSFetchedResultsController is initially fetching the data, it correctly returns all CardApplications that have a definition set and the stateInt has the value 0. But when I change the value of stateInt during runtime to another value that is not matching the predicate condition anymore, the NSFetchedResultsControllerDelegate is not being called reflecting that change in the data. I made sure that I save the managed object's context after changing the stateInt value and I can also see that the context hasChanges before saving and the CardApplication managed object is also showing the change on property changedValues(). From debugger console before saving the change, where self is the CardApplication object I changed stateInt of:
(lldb) po managedObjectContext!.hasChanges
true
(lldb) po self.isUpdated
true
(lldb) po self.changedValues()
▿ 1 element
▿ 0 : 2 elements
- key : "stateInt"
- value : 1
Why is the delegate not being called?
As your FRC is configured to fetch CardIndex objects, it only observes changes to those objects. It will consequently not respond to changes to the related CardApplication objects. You could either deliberately “dirty” the CardIndex, which will trigger the FRC to re-evaluate the predicate, or abandon the FRC and use your own observer to update your UI.
I'm trying to save my model with fields of type bool. When I try to add the "true" value an error occurs.
I also tried to change the field type to Objective C Bool (ObjCBool) unsuccessfully, someone had this problem too?
import UIKit
import CoreData
#objc(Test)
class Test: NSManagedObject {
#NSManaged var title: String
#NSManaged var field: ObjCBool // Or Bool
}
// the error occurs before saving at this point
test.field = true
solution with numberWithBool:
import UIKit
import CoreData
#objc(Test)
class Test: NSManagedObject {
#NSManaged var title: String
#NSManaged var field: NSNumber
}
test.field = NSNumber.numberWithBool(false)
In CoreData, I have defined an unordered to-many relationship from Node to Tag. I've created an Swift entity like this:
import CoreData
class Node : NSManagedObject {
#NSManaged var tags : Array<Tag>
}
Now I want to add a Tag to an instance of Node, like this:
var node = NSEntityDescription.insertNewObjectForEntityForName("Node", inManagedObjectContext: managedObjectContext) as Node
node.tags.append(tag)
However, this fails with the following error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Unacceptable type of value for to-many relationship: property = "tags"; desired type = NSSet; given type = _TtCSs22ContiguousArrayStorage000000000B3440D4; value = (
"<_TtC8MotorNav3Tag: 0xb3437b0> (entity: Tag; id: 0xb343800 ; data: {...})"
).'
What is the correct type for to-many relationships?
To be able to work with one-to-many relationship in Swift you need to define property as:
class Node: NSManagedObject {
#NSManaged var tags: NSSet
}
If you try to use NSMutableSet changes will not be saved in CoreData. And of course it is recommended to define reverse link in Node:
class Tag: NSManagedObject {
#NSManaged var node: Node
}
But still Swift cannot generate dynamic accessors in runtime, so we need to define them manually. It is very convenient to define them in class extension and put in Entity+CoreData.swift file. Bellow is content of Node+CoreData.swift file:
extension Node {
func addTagObject(value:Tag) {
var items = self.mutableSetValueForKey("tags");
items.addObject(value)
}
func removeTagObject(value:Tag) {
var items = self.mutableSetValueForKey("tags");
items.removeObject(value)
}
}
Usage:
// somewhere before created/fetched node and tag entities
node.addTagObject(tag)
Important: To make it all work you should verify that class names of entities in you CoreData model includes your module name. E.g. MyProjectName.Node
As of Xcode 7 and Swift 2.0, the release note 17583057 states:
The NSManaged attribute can be used with methods as well as
properties, for access to Core Data’s automatically generated
Key-Value-Coding-compliant to-many accessors.
#NSManaged var employees: NSSet
#NSManaged func addEmployeesObject(employee: Employee)
#NSManaged func removeEmployeesObject(employee: Employee)
#NSManaged func addEmployees(employees: NSSet)
#NSManaged func removeEmployees(employees: NSSet)
These can be declared in your NSManagedObject subclass. (17583057)
So you just have to declare the following methods and CoreData will take care of the rest:
#NSManaged func addTagsObject(tag: Tag)
#NSManaged func removeTagsObject(tag: Tag)
#NSManaged func addTags(tags: NSSet)
#NSManaged func removeTags(tags: NSSet)
Actually you can just define:
#NSManaged var employees: Set<Employee>
And use the insert and remove methods of the Set directly.
Building on #Keenle's answer, if you want to be cheeky and concise and be able to say
node.tags.append(tag)
one can wrap the call to self.mutableSetValueForKey:
class Node: NSManagedObject {
var tags: NSMutableOrderedSet {
return self.mutableOrderedSetValueForKey("tags")
}
}