How to store a non-standard persistent attribute in Core Data (ex. NSRect)? - macos

I'm struggling with my first Core Data app and not having the smoothest ride :o(
I have a drawing app with a baseclass called DrawingObject which subclasses NSManagedObject and has two properties:
#NSManaged var frameAsValue: NSValue
var frame: NSRect
DrawingObject has a subclass DrawingRectangle. All have corresponding entities with fully qualified classnames set. The frameAsValue attribute is marked as transformable and frame is marked as Undefined transient. The problem is that I get an unrecognised selector error for the frameAsValue property when creating a DrawingRectangle.
I've seen suggestions to transform NSRect to a string to save it to Core Data but this seems error prone (localization) and hackish (if thats a proper word ;o). Here is the code for DrawingObject:
class DrawingObject: NSManagedObject {
#NSManaged var frameAsValue: NSValue
var frame: NSRect = NSZeroRect {
didSet {
frameAsValue = NSValue(rect: frame)
}
}
override func awakeFromInsert() {
frame = NSMakeRect(0, 0, 0, 0)
}
override func awakeFromFetch() {
frame = frameAsValue.rectValue
}
}
I'm now assuming that you have to declare all of the inherited properties in the class hierarchy in each entity. I don't have time to test this now, but will be back soon.

You have to set parent entity of DrawingRectangle to DrawingObject to be able to inherit the properties from the parent entity (just like in the class-hierarchy). You also have to set parent--child entity relationships if you intend to save different subclasses of a parent-entity/parent-class into the same to-many relationship.
For examble if you want to save different subclasses of DrawingObjects in a #NSManaged var objects: NSSet in a Drawing for example, you have to set the parent relationships on the entities to correspond to your NSManagedObject class-hierarchy. So in this case you would set the parent of the DrawingRectangle entity to DrawingObject.
Otherwise NSRect is saved as in the code shown in the question by setting the frame-property of the DrawingObject-entity to transient and optional and the frameAsValue as transformable. You should not specify a valuetransformer so Core Data will use the default value transformer which works perfectly in this case.

Related

How can I use a Transformable attribute as its actual type in a NSManagedObject subclass?

Just starting a project. The data-model file has one entity, which has a single attribute that is Transformable. It's supposed to be a NS/CGRect. I had Xcode create corresponding NSManagedObject subclass files. For "MyThing.swift", I got:
import Foundation
import CoreData
class MyThing: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
}
And I got a "MyThing+CoreDataProperties.swift":
import Foundation
import CoreData
extension MyThing {
#NSManaged var myBounds: NSObject?
}
I want the property to be an actual CGRect, so I need a NSData <-> NSValue <-> NSRect conversion chain. I already have "NSKeyedUnarchiveFromDataTransformer" as the Name under the Attribute Type in Interface Builder. What do I add (and/or change) to pass CGRect values around?
Or do I not do this, and just pass NSValue-wrapped CGRects around instead? (I hope that CoreData will take care of any NSData <-> NSValue conversions.)
As long as your transformable type complies to NSCoding, Core Data will take care of the rest for you. I've used NSAttributedString in my NSManagedObjects, and only changed the CoreData provided id-type that was generated at runtime.
If you want to have a property of your own type, i.e. MyAwesomeObject, then make sure you implement initWithCoder: and encodeWithCoder:.
So, for your case, in order to store CGRects with CoreData, you would need to wrap them in a class that implements the above mentioned methods. This is because CGRect doesn't comply to the NSCoding protocol, and thus cannot be stored directly as a transformable attribute.
Your other option is of course to store x,y,width,height as properties, and either have a transient property wich you compute in awakeFromFetch and awakeFromInsert or just a convenience method.
Because CGRect is not a class but struct i would suggest creating another property which will access the raw variable with proper conversions. And then use only that property.
class MyThing {
var boundsValue : NSValue?
}
extension MyThing {
var bounds : CGRect {
get {
if let value = boundsValue {
value.CGRectValue()
}
return CGRectNull
}
set {
boundsValue = NSValue(CGRect: newValue)
}
}
}

How to apply NSCoding to a class?

I have the following class. When I try to add it to NSUserDefaults like this:
let testClass = TestClass()
testClass.Property1 = 22.3
NSUserDefaults.standardUserDefaults().setObject(testClass, forKey: "object1")
I get an error:
'NSInvalidArgumentException', reason: 'Attempt to insert non-property
list object for key object1'
class TestClass: NSObject, NSCoding {
var Property1:Double?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
if let priceCoded = aDecoder.decodeObjectForKey("Property1") as? Double {
self.Property1 = priceCoded
}
}
func encodeWithCoder(aCoder: NSCoder){
if let priceEncoded = self.Property1 {
aCoder.encodeObject(priceEncoded, forKey: "Property1")
}
}
}
Any idea what I'm doing wrong?
Any idea what I'm doing wrong?
Yes. Not to state the obvious, but "attempt to insert non-property list object" pretty much covers it. The kinds of objects you can insert into the defaults system is limited to property-list types: NSString, NSDate, NSNumber, NSData, and collections NSArray and NSDictionary. You're apparently trying to insert some other kind of object, thus the error.
If you want to store some other kind of object, you'll need to somehow transform it into one of the supported types. You're on the right track -- the usual way to do that is to serialize the object using NSKeyedArchiver, which can store objects that adopt the NSCoding protocol. The idea is that you create a keyed archiver, use it to serialize the object(s) in question, and get back an instance of NSData which you can then store. To get the object back, you do the opposite: use a NSKeyedUnarchiver to deserialize the data object back into the original object.
It looks like you were expecting NSUserDefaults to serialize NSCoding-compliant objects for you, but that doesn't happen -- you need to do it yourself.
There are plenty of examples around, but the usual place to look for this would be the Archives and Serializations Programming Guide. Specifically, check out the "Creating and Extracting Archives" section for sample code. Here's a piece of the relevant example for serializing:
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:aPerson forKey:ASCPersonKey];
[archiver finishEncoding];
Remember that in order to serialize an object this way, that object has to be an instance of a class that adopts the NSCoding protocol. The same document explains how to implement the necessary NSCoding methods, and there must be dozens of questions about it here on SO as well.

Adopting NSTextFinderBarContainer protocol in Swift forces variable initialization despite header comment

I have an NSView subclass that implements the NSTextFinderBarContainer protocol. Part of the NSTextFinderBarContainer protocol is implementing
var findBarView: NSView { get set }
However the comment above this property in the original Objective-C header is:
This property is used by NSTextFinder to assign a find bar to a
container. The container may freely modify the view's width, but
should not modify its height. This property is managed by
NSTextFinder. You should not set this property.
Because Swift requires all instance variables to be initialized, how do I handle this situation? It appears Swift requires me to go against what Apple has wrote in the header: you should not set this property as it will be set/managed by the NSTextFinder itself.
If I don't override the NSView initializers I get:
Class 'ExampleContainerView' has no initializers
As expected since findBarView does not have an initial value.
The relevant parts of my Swift code are:
class ExampleContainerView: NSView, NSTextFinderBarContainer {
var findBarView : NSView
...
}
If I override the designated initializer to initialize findBarView as follows (ignoring Apple's comment in the header):
required init?(coder: NSCoder) {
findBarView = NSView(frame: NSRect())
super.init(coder: coder)
}
The app crashes after the NSTextFinder is sent the setFindBarContainer: message
-[NSView _setTextFinder:]: unrecognized selector sent to instance 0x6000001278a0
The object at 0x6000001278a0 is the NSView instance set in the overridden initializer above.
This appears fixed as of Xcode 7.0 beta 6. NSTextFinderBarContainer now declares findBarView as an optional NSView:
public var findBarView: NSView? { get set }
In addition, contentView() also changed to return an optional NSView:
optional public func contentView() -> NSView?
Making the property optional means there is no longer the contradiction of having the API comments say not to set findBarView, while having Swift require that all non-optional properties are initialized in in your initializers.

Swift + Xcode 6 beta 3 + Core Data = awakeFromInsert not called?

Need help.
I'm creating new Document-based Core Data Cocoa project.
Add entity named 'Entity' into the core data model. Add 'creationDate' propery into it and set its type as Date. And create NSManagedObject subclass from 'Editor' menu.
Now I add into 'Entity.swift' file this code:
override func awakeFromInsert() {
super.awakeFromInsert()
self.creationDate = NSDate()
println("awakeFromInsert called")
}
Now in my NSPersistentDocument subclass I write such a init() method:
init() {
super.init()
var context = self.managedObjectContext
context.undoManager.disableUndoRegistration()
var entity = NSEntityDescription.insertNewObjectForEntityForName("Entity", inManagedObjectContext: context)
context.processPendingChanges()
context.undoManager.enableUndoRegistration()
println("\(entity)")
}
Everything compiles... BUT awakeFromInsert is never called! The interesting part is that 'entity' object ain't nil! It was created, but not initialized. And if I write this line in init method
entity.creationDate = NSDate()
then creationDate property will be set to a current date as expected.
But that's not all. If I debug execution step-by-step I can see that execution enters 'Entity.swift' file, but starts from the top of the file, then immediately drops and returns back to the NSPersistentDocument subclass file.
Tell me, is it a bug? Because I'm tired to fight with this nonsense. Thanks.
Accidentally I got it work: you have to add #objc(YourSubclass) before subclass declaration. I usually did #objc class MySubclass and turned out it does not work (don't know why).
WORKING:
#objc(YourSubclass)
class YourSubclass : NSManagedObject {
...
NOT WORKING:
#objc class YourSubclass : NSManagedObject {
...

How can I change the type of a ViewController after creation?

I have a program that creates UIViewControllers in a UISplitView depending upon which row is selected in a UITableView. For example, if the user clicks the row called 'settings', the program creates a view controller called settingsTableVC (which displays in the left-hand pane of the UISplitViewController) and a detail view controller called settingsDetailVC, which appears in the right hand pane.
The logic works fine, but I have a lot of code for each row in the initial tableView, for creating and setting up each ViewController, and their public properties and it seems to me a simpler way would be to define two generic ViewControllers, then cast them to the correct type in a method. That way, I can define their public properties generically (as they are all basically the same properties, pointers to the masterViewController and the tableViewController)
E.G.
UIViewController *leftController = nil;
UIViewController *rightController = nil;'
Then, in the didSelectRowAtIndexPath:
leftController = [[SettingTableViewController alloc]init];
rightController = [[SettingsDetailViewController alloc]init];
leftController.masterVC = self;
rightController.tableVC = leftController;
However, the complier warns - 'UIViewController does not declare a property masterVC'.
How can I change the class of these ViewControllers at runtime?
You can do either of the following:
SettingsTableViewController *leftControllerCasted = (SettingsTableViewController *)leftController;
SettingsDetailViewController *rightControllerCasted = (SettingsDetailViewController *)rightController;
leftControllerCasted.masterVC = self;
rightControllerCasted.tableVC = leftController; // Doesn't matter if you assign it to leftController or leftControllerCasted
or
((SettingsTableViewController *)leftController).masterVC = self;
((SettingsDetailViewController *)rightController).tableVC = leftController;
You have created your objects correctly, but as you assign them a UIViewController pointer object type the compiler doesn't know your objects are of your custom class.

Resources