I have some problems about the NSMutableSet in Objective-C.
I learnt that the NSSet will compare the two objects' hash code to decide whether they are identical or not.
The problems is, I implemented a class that is subclass of NSObject myself. There is a property NSString *name in that class. What I want to do is when instances of this custom class has the same variable value of "name" , they should be identical, and such identical class should not be duplicated when adding to an NSMutableSet.
So I override the - (NSUInteger)hash function, and the debug shows it returns the same hash for my two instances obj1, obj2 (obj1.name == obj2.name). But when I added obj1, obj2 to an NSMutableSet, the NSMutableSet still contained both obj1, obj2 in it.
I tried two NSString which has the same value, then added them to NSMutableSet, the set will only be one NSString there.
What could be the solution? Thank you for any help!
The custom Class:
Object.h:
#import <Foundation/Foundation.h>
#interface Object : NSObject
#property (retain) NSString *name;
#end
Object.m
#implementation Object
#synthesize name;
-(BOOL)isEqualTo:(id)obj {
return [self.name isEqualToString:[(Object *)obj name]] ? true : false;
}
- (NSUInteger)hash {
return [[self name] hash];
}
#end
and main:
#import <Foundation/Foundation.h>
#import "Object.h"
int main(int argc, const char * argv[])
{
#autoreleasepool {
Object *obj1 = [[Object alloc]init];
Object *obj2 = [[Object alloc]init];
obj1.name = #"test";
obj2.name = #"test";
NSMutableSet *set = [[NSMutableSet alloc] initWithObjects:obj1, obj2, nil];
NSLog(#"%d", [obj1 isEqualTo:obj2]);
NSLog(#"%ld", [set count]);
}
return 0;
}
Instead of implementing isEqualTo: you have to implement isEqual:
- (BOOL)isEqual:(id)object {
return [object isKindOfClass:[MyObject class]] &&
[self.name isEqual:[(MyObject *)object name]];
}
This will (probably falsely) return NO if both self.name and object.name are nil. If you want to return YES if both properties are nil you should use
- (BOOL)isEqual:(id)object {
if ([object isKindOfClass:[MyObject class]]) {
return (!self.name && ![(MyObject *)object name]) ||
[self.name isEqual:[(MyObject *)object name]];
}
return NO;
}
Related
Though it's kind of stupid in 2020 that I'm still asking question about ObjC, please be patient and considerate...
I'm reading the source code of BloksKit and ran into a weird situation.
#import <objc/runtime.h>
#interface _WeakAssociatedObjectWrapper : NSObject
#property (nonatomic, weak) id object;
#end
#implementation _WeakAssociatedObjectWrapper
#end
#interface NSObject (AddWeak)
#end
#implementation NSObject (AddWeak)
- (void)setWeakProp:(id)weakProp {
_WeakAssociatedObjectWrapper *wrapper = objc_getAssociatedObject(self, #selector(weakProp));
if (!wrapper) {
wrapper = [[_WeakAssociatedObjectWrapper alloc] init];
objc_setAssociatedObject(self, #selector(weakProp), wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
wrapper.object = weakProp;
}
- (id)weakProp {
id value = objc_getAssociatedObject(self, _cmd);
if ([value isKindOfClass:_WeakAssociatedObjectWrapper.class]) {
return [(_WeakAssociatedObjectWrapper *)value object];
}
return value;
}
#end
int main(int argc, const char * argv[]) {
#autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
{
NSObject *prop = [[NSObject alloc] init];
[obj setWeakProp:prop];
[obj weakProp]; // *Weird!!
}
NSLog(#"Now obj.weakProp = %#", [obj weakProp]);
}
return 0;
}
This code is adding a weak associated object for category.(BlocksKit does so)
Note the *Weird!! line. If this line is commented out, then it prints (null), which is reasonable since prop is deallocated outside the {} scope. On the other side, if not commented out, it prints <NSObject: 0xxxxx>, which indicates that prop is somehow retained by someone(Or any other reason?).
What is happening here??! (BlocksKit behaves the same!)
Environment: XCode 10.3
This is a feature. For the case (and any similar)
[obj weakProp];
by properties/accessors naming convention ARC returns autoreleased instance, so in your case #autoreleasepool holds it and testing as below can show this.
int main(int argc, const char * argv[]) {
NSObject *obj = [[NSObject alloc] init];
#autoreleasepool {
{
NSObject *prop = [[NSObject alloc] init];
[obj setWeakProp:prop];
[obj weakProp]; // *Weird!!
}
NSLog(#"Now obj.weakProp = %#", [obj weakProp]);
}
NSLog(#"After autoreleased >> obj.weakProp = %#", [obj weakProp]);
return 0;
}
This is code from an addition calculator that does operations by entering the two operands first and then the operation; like "5 enter 2 enter +" would result in "7". When the user taps on a number a double will be sent to pushOperand: When a user taps on the addition button the string #"+" will be sent like to performOperation:. My question is what is the point of making those copies in program and runProgram: if they're all shallow copies and their elements all end up pointing to the same elements of NSNumber and NSString objects as _programStack, program, and stack?
#import <Foundation/Foundation.h>
#interface CalculatorBrain : NSObject
#property (nonatomic, readonly) id program;
+(double)runProgram:(id)program;
-(double)performOperation:(NSString *)operation;
#end
#import "CalculatorBrain.h"
#interface CalculatorBrain ()
#property (nonatomic, strong) NSMutableArray *programStack;
#end
#implementation CalculatorBrain
#synthesize programStack = _programStack;
-(NSMutableArray *) programStack {
if (!_programStack)
_programStack = [[NSMutableArray alloc] init];
return _programStack;
}
-(void)pushOperand:(double)operand {
[self.programStack addObject: [NSNumber numberWithDouble: operand]];
}
-(double)performOperation:(NSString *)operation {
[self.programStack addObject: operation];
double result = [CalculatorBrain runProgram: self.program];
return result;
}
-(id)program {
return [self.programStack copy];
}
+(double)runProgram:(id)program {
NSMutableArray *stack;
if ([program isKindOfClass: [NSArray class]])
stack = [program mutableCopy];
return [self popOperandOffProgramStack: stack];
}
+(double)popOperandOffProgramStack:(NSMutableArray *)stack {
double result = 0;
id topOfStack = [stack lastObject];
if (topOfStack)
[stack removeLastObject];
if ([topOfStack isMemberOfClass: [NSNumber class]])
result = [topOfStack doubleValue];
if ([topOfStack isKindOfClass: [NSString class]]) {
NSString *operation = topOfStack;
if ([operation isEqualToString: #"+"]) {
result = [self popOperandOffProgramStack: stack] + [self popOperandOffProgramStack: stack];
}
return result;
}
#end
NSNumber and NSString are immutable so making a shallow copy of a collection of objects that can't changee is safe.
In program it is important to return a copy of programStack rather than the the actual mutable array. This is because programStack is a private internal property declared in a class extension so it is not externally visible. If you returned programStack directly an external user could change it since it is an NSMutableArray. The program method returns an NSArray since copies of mutable objects are immutable, which has the right semantics. You want to give the external user a snapshot of the programStack array, not access to your class internals.
In runProgram the situation is different. The external user passes CalculatorBrain an NSArray to process and the class' internal logic requires that the stack have elements popped off the array as it is processed. Thus you need to make a mutableCopy so that it can be mutated for processing.
I have a class that has a NSMutableDictionary as a property:
#interface Alibi : NSObject <NSCopying>
#property (nonatomic, copy) NSMutableDictionary * alibiDetails;
#end
With the following constructor:
- (Alibi *)init
{
self = [super init];
_alibiDetails = [NSMutableDictionary dictionary];
return self;
}
and copy method:
- (Alibi *)copyWithZone:(NSZone *)zone
{
Alibi *theCopy = [[Alibi alloc] init];
theCopy.alibiDetails = [self.alibiDetails mutableCopy];
return theCopy;
}
When I try to call setObject:ForKey: I get a runtime error mutating method sent to immutable object.
I have the Alibi object declared in the view controller as #property (copy, nonatomic) Alibi * theAlibi; and I initialize it with self.theAlibi = [[Alibi alloc] init]; in viewDidLoad.
The line which crashes is:
NSString * recipient;
recipient = #"Boss";
[self.theAlibi.alibiDetails setObject:recipient forKey:#"Recipient"];
Please let me know what I am doing wrong here. I am coding for iOS 5 on iPhone.
You have a 'copy' property, which means exactly that - your NSMutableDictionary will get the -copy method called and return a regular NSDictionary before being assigned to the synthesized instance variable. This thread provides some information on some of your options as to solving this.
For the sake of completing this thread I will include my revised Alibi class below, this works as I require it to. If anyone notices any memory leaks or other issues, that would be appreciated.
#implementation Alibi
NSMutableDictionary *_details;
- (Alibi *)init
{
self = [super init];
_details = [NSMutableDictionary dictionary];
return self;
}
- (NSMutableDictionary *)copyDetails
{
return [_details mutableCopy];
}
- (NSMutableDictionary *)setDetails:(NSMutableDictionary *)value
{
_details = value;
return value;
}
- (void)addDetail:(id)value forKey:(id)key
{
[_details setObject:value forKey:key];
}
- (id)getDetailForKey:(id)key
{
return [_details objectForKey:key];
}
- (Alibi *)copyWithZone:(NSZone *)zone
{
Alibi *theCopy = [[Alibi alloc] init];
theCopy.serverId = [self.serverId copyWithZone:zone];
theCopy.user = [self.user copyWithZone:zone];
theCopy.startTime = [self.startTime copyWithZone:zone];
theCopy.endTime = [self.endTime copyWithZone:zone];
[theCopy setDetails:[self copyDetails]];
return theCopy;
}
#end
How can I call a method with multiple params like below with performSelectorInBackground?
Sample method:
-(void) reloadPage:(NSInteger)pageIndex firstCase:(BOOL)firstCase;
The problem is that performSelectorInBackground:withObject: takes only one object argument. One way to get around this limitation is to pass a dictionary (or array) of arguments to a "wrapper" method that deconstructs the arguments and calls your actual method:
- (void)callingMethod {
NSDictionary * args = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInteger:pageIndex], #"pageIndex",
[NSNumber numberWithBool:firstCase], #"firstCase",
nil];
[self performSelectorInBackground:#selector(reloadPageWrapper:)
withObject:args];
}
- (void)reloadPageWrapper:(NSDictionary *)args {
[self reloadPage:[[args objectForKey:#"pageIndex"] integerValue]
firstCase:[[args objectForKey:#"firstCase"] boolValue]];
}
- (void)reloadPage:(NSInteger)pageIndex firstCase:(BOOL)firstCase {
// Your code here...
}
This way you're only passing a "single" argument to the backgrounding call, but that method can construct the multiple arguments you need for the real call (which will take place on the same backgrounded thread).
I've just found this question and wasn't happy with any of the answers. In my opinion neither make good use of the tools available, and passing around arbitrary information in arrays and dictionaries generally worries me.
So, I went and wrote a small NSObject category that will invoke an arbitrary selector with a variable number of arguments:
Category Header
#interface NSObject (NxAdditions)
-(void)performSelectorInBackground:(SEL)selector withObjects:(id)object, ... NS_REQUIRES_NIL_TERMINATION;
#end
Category Implementation
#implementation NSObject (NxAdditions)
-(void)performSelectorInBackground:(SEL)selector withObjects:(id)object, ...
{
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
// Setup the invocation
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = self;
invocation.selector = selector;
// Associate the arguments
va_list objects;
va_start(objects, object);
unsigned int objectCounter = 2;
for (id obj = object; obj != nil; obj = va_arg(objects, id))
{
[invocation setArgument:&obj atIndex:objectCounter++];
}
va_end(objects);
// Make sure to invoke on a background queue
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithInvocation:invocation];
NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init];
[backgroundQueue addOperation:operation];
}
#end
Usage
-(void)backgroundMethodWithAString:(NSString *)someString array:(NSArray *)array andDictionary:(NSDictionary *)dict
{
NSLog(#"String: %#", someString);
NSLog(#"Array: %#", array);
NSLog(#"Dict: %#", dict);
}
-(void)someOtherMethod
{
NSString *str = #"Hello world";
NSArray *arr = #[#(1337), #(42)];
NSDictionary *dict = #{#"site" : #"Stack Overflow",
#"url" : [NSURL URLWithString:#"http://stackoverflow.com"]};
[self performSelectorInBackground:#selector(backgroundMethodWithAString:array:andDictionary:)
withObjects:str, arr, dict, nil];
}
Well, I have used this:
[self performSelectorInBackground:#selector(reloadPage:)
withObject:[NSArray arrayWithObjects:pageIndex,firstCase,nil] ];
for this:
- (void) reloadPage: (NSArray *) args {
NSString *pageIndex = [args objectAtIndex:0];
NSString *firstCase = [args objectAtIndex:1];
}
with performSelectorInBackground you can only pass one argument, so make a custom object for this method to hold your data, itll be more concise than an ambiguous dictionary or array. The benefit of this is you can pass the same object around when done containing several return properties.
#import <Foundation/Foundation.h>
#interface ObjectToPassToMethod : NSObject
#property (nonatomic, strong) NSString *inputValue1;
#property (nonatomic, strong) NSArray *inputArray;
#property (nonatomic) NSInteger returnValue1;
#property (nonatomic) NSInteger returnValue2;
#end
and pass that object to your method:
ObjectToPassToMethod *obj = [[ObjectToPassToMethod alloc] init];
obj.inputArray = #[];
obj.inputValue1 = #"value";
[self performSelectorInBackground:#selector(backgroundMethod:) withObject:obj];
-(void)backgroundMethod:(ObjectToPassToMethod*)obj
{
obj.returnValue1 = 3;
obj.returnValue2 = 90;
}
make sure to clean up the object when done to prevent memory leaks
Is there any way to access an NSArray element with valueForKeyPath? Google's reverse geocoder service, for example, returns a very complex data structure. If I want to get the city, right now I have to break it into two calls, like this:
NSDictionary *address = [NSString stringWithString:[[[dictionary objectForKey:#"Placemark"] objectAtIndex:0] objectForKey:#"address"]];
NSLog(#"%#", [address valueForKeyPath:#"AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName"]);
Just wondering if there's a way to shoehorn the objectAtIndex: call into the valueForKeyPath string. I tried a javascript-esque formulation like #"Placemark[0].address" but no dice.
Unfortunately, no. The full documentation for what's allowed using Key-Value Coding is here. There are not, to my knowledge, any operators that allow you to grab a particular array or set object.
Here's a category I just wrote for NSObject that can handle array indexes so you can access a nested object like this: "person.friends[0].name"
#interface NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath;
#end
#import "NSObject+ValueForKeyPathWithIndexes.h"
#implementation NSObject (ValueForKeyPathWithIndexes)
-(id)valueForKeyPathWithIndexes:(NSString*)fullPath
{
NSRange testrange = [fullPath rangeOfString:#"["];
if (testrange.location == NSNotFound)
return [self valueForKeyPath:fullPath];
NSArray* parts = [fullPath componentsSeparatedByString:#"."];
id currentObj = self;
for (NSString* part in parts)
{
NSRange range1 = [part rangeOfString:#"["];
if (range1.location == NSNotFound)
{
currentObj = [currentObj valueForKey:part];
}
else
{
NSString* arrayKey = [part substringToIndex:range1.location];
int index = [[[part substringToIndex:part.length-1] substringFromIndex:range1.location+1] intValue];
currentObj = [[currentObj valueForKey:arrayKey] objectAtIndex:index];
}
}
return currentObj;
}
#end
Use it like so
NSString* personsFriendsName = [obj valueForKeyPathsWithIndexes:#"me.friends[0].name"];
There's no error checking, so it's prone to breaking but you get the idea.
You can intercept the keypath in the object holding the NSArray.
In your case the keypath would become Placemark0.address... Override valueForUndefinedKey; look for the index in the keypath; something like this:
-(id)valueForUndefinedKey:(NSString *)key
{
// Handle paths like Placemark0, Placemark1, ...
if ([key hasPrefix:#"Placemark"])
{
// Caller wants to access the Placemark array.
// Find the array index they're after.
NSString *indexString = [key stringByReplacingOccurrencesOfString:#"Placemark" withString:#""];
NSInteger index = [indexString integerValue];
// Return array element.
if (index < self.placemarks.count)
return self.placemarks[index];
}
return [super valueForUndefinedKey:key];
}
This works really well for model frameworks e.g. Mantle.
Subclass NSArrayController or NSDictionaryController
Use NSArrayController for this purpose, because NSObjectController does not include NSArrayController's provided handling of changes to bound array elements. If you use this same code with NSObjectController instead, then using Cocoa Bindings with your NSObjectController instance will only set the (bound interface element's) value at the time of binding but will not receive the messages from array elements in return. By using NSObjectController for this purpose, the user interface will not continue to update even though the contentObject is updated. Simply use the same code with NSArrayController to also include proper support for arrays -- which is the matter at hand.
#import <Cocoa/Cocoa.h>
#interface DelvingArrayController : NSArrayController
#end
#import "DelvingArrayController.h"
#implementation DelvingArrayController
-(id)valueForKeyPath:(NSString *)keyPath
{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"^(.+?)\\[(\\d+?)\\]$" options:NSRegularExpressionCaseInsensitive error:&error];
NSArray<NSString*> *components = [keyPath componentsSeparatedByString:#"."];
id currentObject = self;
for (NSUInteger i = 0; i < components.count; i++)
{
if (![components[i] isEqualToString:#""])
{
NSTextCheckingResult *check_result = [regex firstMatchInString:components[i] options:0 range:NSMakeRange(0, components[i].length)];
if (!check_result)
currentObject = [currentObject valueForKey:components[i]];
else
{
NSRange array_name_capture_range = [check_result rangeAtIndex:1];
NSRange number_capture_range = [check_result rangeAtIndex:2];
if (number_capture_range.location == NSNotFound)
currentObject = [currentObject valueForKey:components[i]];
else if (array_name_capture_range.location != NSNotFound)
{
NSString *array_name = [components[i] substringWithRange:array_name_capture_range];
NSUInteger array_index = [[components[i] substringWithRange:number_capture_range] integerValue];
currentObject = [currentObject valueForKey:array_name];
if ([currentObject count] > array_index)
currentObject = [currentObject objectAtIndex:array_index];
}
}
}
}
return currentObject;
}
//at some point... also override setValueForKeyPath :-)
#end
This code uses NSRegularExpression, which is for macOS 10.7+.
I leave it as an exercise for you to use the same approach to also override setValueForKeyPath, if you want write functionality.
Cocoa Bindings Example Usage
Say we want a little trivia game, with a window that shows a question and uses four buttons to display multiple-choice options. We have the questions and multiple-choice options as NSStrings in a plist, and also an NSNumber or optionally BOOL entries to indicate the correct answers. We want to bind the option buttons to options in the array, for each question also stored in an array.
Here is the example plist containing some trivia questions related to the game Halo. Notice that the options are located within nested arrays.
In this example, I use NSObjectController *stringsController as the controller for the entire plist file, and DelvingArrayController *triviaController as the controller for the trivia-related plist entries. You might simply use one DelvingArrayController instead, but I provide this for your understanding.
The trivia window is really simple, so I merely design it using Interface Builder in MainMenu.xib:
A subclass of NSDocumentController is used for showing the trivia window via an NSMenuItem added in Interface Builder. The instance of this subclass is also in the .xib, so if we want to use the interface elements in the .xib, we have to wait for the Application Delegate instance's - (void)applicationDidFinishLaunching:(NSNotification *)aNotification method or otherwise wait until the .xib has finished loading...
#import <Cocoa/Cocoa.h>
#import "MenuInterfaceDocumentController.h"
#interface AppDelegate : NSObject <NSApplicationDelegate>
#property IBOutlet MenuInterfaceDocumentController *PrimaryInterfaceController;
#end
#import "AppDelegate.h"
#interface AppDelegate ()
#end
#implementation AppDelegate
#synthesize PrimaryInterfaceController;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
if ([NSApp mainMenu])
{
[PrimaryInterfaceController configureTriviaWindow];
}
}
#import <Cocoa/Cocoa.h>
#interface MenuInterfaceDocumentController : NSDocumentController
{
IBOutlet NSMenuItem *MenuItemTrivia; // shows the Trivia window
IBOutlet NSWindow *TriviaWindow;
IBOutlet NSTextView *TriviaQuestionField;
IBOutlet NSButton *TriviaOption1, *TriviaOption2, *TriviaOption3, *TriviaOption4;
}
#property NSObjectController *stringsController;
-(void)configureTriviaWindow;
#end
#import "MenuInterfaceDocumentController.h"
#interface MenuInterfaceDocumentController ()
#property NSDictionary *languageDictionary;
#property DelvingArrayController *triviaController;
#property NSNumber *triviaAnswer;
#end
#implementation MenuInterfaceDocumentController
#synthesize stringsController, languageDictionary, triviaController, triviaAnswer;
// all this happens before the MainMenu is available, and before the AppDelegate is sent applicationDidFinishLaunching
-(instancetype)init
{
self = [super init];
if (self)
{
if (!stringsController)
stringsController = [NSObjectController new];
stringsController.editable = NO;
// check for the plist file, eventually applying the following
languageDictionary = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"en" ofType:#"plist"]];
if (languageDictionary)
[stringsController setContent:languageDictionary];
if (!triviaController)
{
triviaController = [DelvingArrayController new];
[triviaController bind:#"contentArray" toObject:stringsController withKeyPath:#"selection.trivia" options:nil];
}
triviaController.editable = NO;
if (!triviaAnswer)
{
triviaAnswer = #0;
[self bind:#"triviaAnswer" toObject:triviaController withKeyPath:#"selection.answer" options:nil];
}
}
return self;
}
// if we ever do something like change the plist file to a duplicate plist file that is in a different language, use this kind of approach to keep the same trivia entry active
-(IBAction)changeLanguage:(id)sender
{
NSUInteger triviaQIndex = triviaController.selectionIndex;
if (sender == MenuItemEnglishLanguage)
{
if ([self changeLanguageTo:#"en" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:#"en"];
if ([triviaController.content count] > triviaQIndex) // in case the plist files don't match
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
else if (sender == MenuItemGermanLanguage)
{
if ([self changeLanguageTo:#"de" Notify:YES])
{
[self updateSelectedLanguageMenuItemWithLanguageString:#"de"];
if ([triviaController.content count] > triviaQIndex)
[triviaController setSelectionIndex:triviaQIndex];
}
else
[self displayAlertFor:CUSTOM_ALERT_TYPE_LANGUAGE_CHANGE_FAILED];
}
}
-(void)configureTriviaWindow
{
[TriviaQuestionField bind:#"string" toObject:triviaController withKeyPath:#"selection.question" options:nil];
[TriviaOption1 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[0]" options:nil];
[TriviaOption2 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[1]" options:nil];
[TriviaOption3 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[2]" options:nil];
[TriviaOption4 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[3]" options:nil];
}
// this method is how you would manually set the value if you did not use binding:
-(void)updateTriviaAnswer
{
triviaAnswer = [triviaController valueForKeyPath:#"selection.answer"];
}
-(IBAction)changeTriviaQuestion:(id)sender
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
-(IBAction)showTriviaWindow:(id)sender
{
[TriviaWindow makeKeyAndOrderFront:sender];
}
- (IBAction)TriviaOptionChosen:(id)sender
{
// tag integers 0 through 3 are assigned to the option buttons in Interface Builder
if ([sender tag] == triviaAnswer.integerValue)
[self changeTriviaQuestion:sender];
else
NSBeep();
}
#end
Summary of Sequence
NSObjectController *stringsController = [[NSObjectController alloc] initWithContent:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"en" ofType:#"plist"]]];
DelvingArrayController *triviaController = [DelvingArrayController new];
[triviaController bind:#"contentArray" toObject:stringsController withKeyPath:#"selection.trivia" options:nil];
NSNumber *triviaAnswer = #0;
[self bind:#"triviaAnswer" toObject:triviaController withKeyPath:#"selection.answer" options:nil];
// bind to .xib's interface elements after the nib has finished loading, else the IBOutlets are null
[TriviaQuestionField bind:#"string" toObject:triviaController withKeyPath:#"selection.question" options:nil];
[TriviaOption1 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[0]" options:nil];
[TriviaOption2 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[1]" options:nil];
[TriviaOption3 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[2]" options:nil];
[TriviaOption4 bind:#"title" toObject:triviaController withKeyPath:#"selection.options[3]" options:nil];
// when the user chooses the correct option, go to the next question
if ([sender tag] == triviaAnswer.integerValue)
{
if (triviaController.selectionIndex >= [(NSArray*)triviaController.content count] - 1)
[triviaController setSelectionIndex:0];
else
[triviaController setSelectionIndex:(triviaController.selectionIndex + 1)];
}
Create methods that supports array for NSObject:
#interface NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path;
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath;
#end
#implementation NSObject(ArraySupported)
-(id)valueForKeySupportedArray:(NSString*)path {
id value = nil;
if ([self isKindOfClass:[NSArray class]]) {
NSArray *array = (NSArray *)self;
NSUInteger index = path.integerValue;
if (index >= 0 && index < array.count) {
value = array[index];
}
} else {
value = [self valueForKey:path];
}
return value;
}
-(id)valueForKeyPathSupportedArray:(NSString*)fullPath {
NSArray* parts = [fullPath componentsSeparatedByString:#"."];
id value = self;
for (NSString* part in parts) {
value = [value valueForKeySupportedArray:part];
if (value == nil) {
break;
}
}
return value;
}
#end
How to use:
NSObject *object = #{#"Placemark":#[#{#"address":#"..."}]};
NSString *address = [object valueForKeyPathSupportedArray:#"Placemark.0.address"];
// address = "..."