I'm a newcomer to Cocoa programming, having never got into Objective-C. Now, I am trying to learn it with Swift by going through the Aaron Hillegrass book “Cocoa Programming for Mac OS X, 4e" and implementing everything there in Swift instead of Obj-C. It has been going okay so far, but I hit a roadblock in Chapter 8 (the RaiseMan application).
Here is the Objective-C code from the book:
The header:
#import <Foundation/Foundation.h>
#interface Person : NSObject {
NSString *personName;
float expectedRaise;
}
#property (readwrite, copy) NSString *personName;
#property (readwrite) float expectedRaise;
#end
And here is the implementation
#import "Person.h"
#implementation Person
#synthesize personName;
#synthesize expectedRaise;
- (id)init
{
self = [super init];
if (self) {
expectedRaise = 0.05;
personName = #"New Person";
}
return self;
}
#end
You are then supposed to go to Interface Builder, get an Array Controller, specify Person as its class, and add personName and expectedRaise as its keys.
I rewrote the Person class in Swift as follows:
import Cocoa
class Person: NSObject {
var personName = String()
var expectedRaise = Float()
}
and connected it to the ArrayController as the book told me to.
There is also some code in the Document file:
init() {
employees = NSMutableArray()
println("hi")
super.init()
// Add your subclass-specific initialization here.
var p = Person()
p.personName = "New Person"
p.expectedRaise = 0.05
}
Here is what the interface looks like (SO won't let me post it directly) https://www.dropbox.com/s/e5busxes9ex3ejd/Screenshot%202014-06-13%2019.02.37.png
When I try to run the app and click the "add employees" button, I get this error in the console:
"RaiseMan[9290:303] Cannot find object class with name Person"
So, my question is: what am I doing wrong?
Short answer: you need to specify the class namespace (module name) in Interface Builder: RaiseMan.Person
Details and other options:
This is because Swift adds a prefix to the name of every class injected into the Objective-C runtime in order to avoid name collisions. The prefix follows this convention: _TtC$$AppName%%ClassName, where $$ is the length of AppName and %% is the length of ClassName (see this other SO question for more info).
So in order for the array controller to be able to instantiate the Person class, you need to provide the mangled name in Interface Builder: _TtC8RaiseMan6Person.
Another option is to provide an explicit Objective-C name for your Swift class by using the #objc(<#name#>) attribute:
#objc(Person)
class Person: NSObject {
}
In that case, you can provide the name specified in your #objc attribute to Interface Builder (e.g. Person). See the Using Swift with Cocoa and Objective-C guide for more details.
If I'm reading your example right, and assuming your init() method belongs in the Person class, you need to put the init() method inside the class braces.
Also, declare variables of a certain type with the following format:
var/let (name): (type)(optional)
So, for your example, you would instead have for the Person class:
class Person {
var personName: String
var expectedRaise: Float?
init() {...}
}
You don't necessarily have to subclass from NSObject if you don't need to; in Swift objects don't have to subclass from NSObject (or another parent) if they don't require it. And the "Float?" means that it is of type Optional Float, meaning that it is a Float that may have a value, or it may have no value, depending if the person is expected to get a raise or not (or you may not want it Optional and just set the value to 0).
Keep in mind, if you have multiple build targets, every new class you create needs to be added to that build target. If you don't have its target membership set to the build target, Swift will not find the class file.
Related
I am using your new BlinkIDUI sdk for iOS and I can have the list of all the scanned fields from "recognitionResult.resultEntries" like Secondary ID = Jason", "Primary ID = Bourne", "Sex = F", "Date Of Birth = 3/23/83", "Nationality = UAE", "Document Code = P" from the delegate method "- (void)didScanEntireDocumentWithRecognitionResult:(MBRecognitionResult * _Nonnull)recognitionResult successFrame:(UIImage * _Nullable)successFrame". My query is How to get value for particular key like “"Document Code” ?
Additional Details are:
The Framework addition in Project: Manual.
Xcode version : 10.1.
Language: Objective-C (ARC OFF).
Device: iPhone8 / iOS(11.1.1)
That's because resultEntries is an array not a dictionary,
Use like this:
for (MBField *field in recognitionResult.resultEntries) {
if (field.key == MBFieldKeyDocumentCode) {
}
}
If you are using it in ObjectiveC project then also check if #objc tag is there in front of MBFieldKey public property in "MBField" class, if it is not there just put it as:
public class MBField: NSObject {
#objc public let key: MBFieldKey
#objc public let value: String
.....
}
How to implement a two way data binding using Swift 2.0?
Let's assume I have the following model class (using couchbase lite):
#objc(Person)
class Person: NSObject{
#NSManaged var firstName: NSString?
#NSManaged var lastName: NSString?
}
And I like to bind the properties to a given formItemDescriptor like this:
class FormItemDescriptor {
var tag: String?
var value: Any?
}
How would I bind the FormItemDescriptor.value property to the Person.firstName property using 2-way-binding?
So changes in the model appear in the form and vice versa?
Swift does not support bindings out of the box, but you can achieve them with frameworks like ReactiveKit.
For example, when you declare an observable property you can bind it to another observable property.
let numberA: Property<Int>
let numberB: Property<Int>
numberA.bindTo(numberB)
To do bi-directional, just bind in other direction too:
numberB.bindTo(numberA)
Now, whenever you change any of them, the other will change.
Your example is bit harder though because you need to do bindings from #NSManaged properties that cannot be made Observable. It's not impossible, but to work the properties of Person should support key-value observing (KVO).
For example, given a Person object and a label, you could take an Property from KVO-enabled property with the method rValueForKeyPath and then bind that observable to the label, like this:
let person: Person
let nameLabel: UILabel
person.rValueForKeyPath("firstName").bindTo(nameLabel)
If you have an intermediately object like your FormItemDescriptor, then you'll have to make its properties observable.
class FormItemDescriptor {
var tag: Property<String?>
var value: Property<Any?>
}
Now you could establish binding
let person: Person
let descriptor: FormItemDescriptor
person.rValueForKeyPath("firstName").bindTo(descriptor.value)
Because firstName is not observable, you cannot do binding in another direction so you'll have to update it manually whenever value changes.
descriptor.value.observeNext { value in
person.firstName = value as? NSString
}
We also had to do cast to NSString because value is of type Any?. You should rethink that though and see if you can make it a stronger type.
Note that UIKit bindings are coming from ReactiveUIKit framework. Check our documentation of ReactiveKit for more info.
The problem I'm having is that i can't assign to an attribute of a subclass unless i've set the subclassed entity's Parent Entity property to the entity of the abstract superclass.
In XCode 4.0.2, this is the Parent Entity property to which i'm referring:
What i don't get is that i thought a Parent Entity is intended for parent-child relationships between entities and the object relationships are simply captured by the class definitions.
Example
Entities A, B and C:
A is an abstract entity of type A with attributes:
y : string
z : string
B is an entity of type A with no attributes
C is an entity of type A with no attributes
Classes A, B and C:
#interface A : NSManagedObject {
}
#property (nonatomic, retain) NSString * y;
#property (nonatomic, retain) NSString * z;
#interface B : A {
}
#interface C : A {
}
The Problem
If i don't set the Parent Entity for entities B and C to be entity A, then when i try:
NSEntityDescription *be = [[mom entitiesByName] objectForKey:#"B"];
B *b = [[NSManagedObject alloc] initWithEntity:be
insertIntoManagedObjectContext:moc];
b.y = #"test"; // <<-- This line causes the following error:
I get:
-[NSManagedObject setY:]: unrecognized selector sent to instance
If i set the Parent Entity, it seems to work except that entity persisted to the store seems to be A instead of B.
Did you remember to set the class for entity B to class B? If yes, you should take a look at the type of the pointer that's assigned to b... is it in fact a B*? It looks like it's probably a NSManagedObject* based on the error you're getting, and according to the docs for -initWithEntity:insertIntoManagedObjectContext:...
The dynamically-generated subclass
will be based on the class specified
by the entity, so specifying a custom
class in your model will supersede the
class passed to alloc.
...I think you should be getting a B* if your model is configured correctly.
I currently have a cardType attribute on my entity, which in the old model could be "Math", "Image" or "Text". In the new model, I'll be using just "Math" and "Text" and also have a hasImage attribute, which I want to set to true if the old cardType was Image (which I want to change to "Text").
Lastly, I have a set of another entity, "card", of which a set can be associated with a deck, and in each of those, I'll also have hasImage which I want to set to true if the deck was of "Image" type before.
Is this all possible using the Value Expression in the Mapping Model I've created between the two versions, or will I have to do something else?
I can't find any document telling me exactly what is possible in the Value Expression (Apple's doc - http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CoreDataVersioning/Articles/vmMappingOverview.html%23//apple_ref/doc/uid/TP40004735-SW3 - only has a very simple transformation). If I have to do something else, what would that be? This seems simple enough that an expression should be able to do it.
One thing you can do is create a custom migration policy class that has a function mapping your attribute from the original value to a new value. For example I had a case where I needed to map an entity called MyItems that had a direct relationship to a set of values entities called "Items" to instead store an itemID so I could split the model across multiple stores.
The old model looked like this:
The new model looks like this:
To do this, I wrote a mapping class with a function called itemIDForItemName and it was defined as such:
#interface Migration_Policy_v1tov2 : NSEntityMigrationPolicy {
NSMutableDictionary *namesToIDs;
}
- (NSNumber *) itemIDForItemName:(NSString *)name;
#end
#import "Migration_Policy_v1tov2.h"
#implementation Migration_Policy_v1tov2
- (BOOL)beginEntityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error {
namesToIDs = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:1],#"Apples",
[NSNumber numberWithInt:2],#"Bananas",
[NSNumber numberWithInt:3],#"Peaches",
[NSNumber numberWithInt:4],#"Pears",
[NSNumber numberWithInt:5],#"Beef",
[NSNumber numberWithInt:6],#"Chicken",
[NSNumber numberWithInt:7],#"Fish",
[NSNumber numberWithInt:8],#"Asparagus",
[NSNumber numberWithInt:9],#"Potato",
[NSNumber numberWithInt:10],#"Carrot",nil];
return YES;
}
- (NSNumber *) itemIDForItemName:(NSString *)name {
NSNumber *iD = [namesToIDs objectForKey:name];
NSAssert(iD != nil,#"Error finding ID for item name:%#",name);
return iD;
}
#end
Then for the related Mapping Name for the attribute in your mapping model you specify the Value Expression as the result of your function call as such:
FUNCTION($entityPolicy,"itemIDForItemName:",$source.name) **
You also have to set the Custom Policy Field of your Mapping Name for that attribute to your mapping class name (in this case Migration_Policy_v1tov2).
**note this should match the selector signature of the method
I have replaced an NSTextField with an NSTokenField so that I can perform some auto-completion. The value of the NSTextField was bound to a NSString attribute of a controller class. Now that I have changed the NSTextField to an NSTokenField the value has changed to an NSArray.
How do I make the NSTokenField value binding be an NSString?
The changing of the value from an NSString to an NSArray seems like bad OO design. I though that a subclass should be able replace a superclass without any modifications to the subclass.
If all you want is autocompletion, and not tokenization, you can achieve this by using a plain NSTextField and implementing the delegate method:
- (NSArray *)control:(NSControl *)control textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index
(This method is actually declared in NSControl, NSTextField's superclass.)
If you do want to have tokenization, then you will have to provide an NSArray for the object value to be displayed in the token field. As explained in the NSTokenField programming guide, the array you provide will be a mix of strings and objects. Strings will be displayed as-is, and any non-string objects will be displayed as tokens in the token field. You would need to implement the various NSTokenField delegate methods to provide a string to be displayed for each represented object in your array.
It does appear that the Cocoa Bindings Reference states that the object bound to the value of an NSTokenField should be a string or number, but in my experience, this is incorrect, and the token field should be bound to an NSArray, just like when using setObjectValue:
You can subclass your own NSValueTransformer and set it in your binding.
NSTokenField's value binding accepts an NSString or NSNumber binding, not an NSArray. How have you determined that it is wanting an NSArray?
The best way to do this (as Cocoafan pointed out) is to use Value Transformers. Value transformers allow you convert the object-type used your model into a type that's suitable for a view. Here is a very simple String/Array transformer that allows you to store your data as a comma separated string but will convert it back and forth to an array of strings.
#interface StringArrayTransformer: NSValueTransformer {}
#end
#implementation StringArrayTransformer
+ (Class)transformedValueClass { return [NSString class]; }
+ (BOOL)allowsReverseTransformation { return YES; }
- (id)transformedValue:(id)value {
NSString *string = (NSString*) value;
return [string componentsSeparatedByString:#", "];
}
-(id)reverseTransformedValue:(id)value {
NSArray *array = (NSArray*)value;
return [array componentsJoinedByString:#", "];
}
#end
If you're using bindings for your NSTokenField then to use this the transformer simply select the NSTokenField in Interface Builder, then in the Bindings Inspector on the right hand side, for the Value binding, set the "Value Transformer" to StringArrayTransformer as per below.