NSPopupButton Bindings with Value Transformer - objective-c

I don't know if what I see with a popup button populated by bindings with a value transformer is the way it's supposed to be or not -- the unusual thing I'm seeing (at least with respect to what I've seen with value transformers and table views) is that the "value" parameter in the transformedValue: method is the whole array bound to the array controller, not the individual strings in the array. When I've done this with table views, the transformer is called once for each displayed row in the table, and the "value" parameter is whatever object is bound to that row and column, not the whole array that serves as the content array for the array controller.
I have a very simple app to test this. In the app delegate there is this:
+(void)initialize {
RDTransformer *transformer = [[RDTransformer alloc] init];
[NSValueTransformer setValueTransformer:transformer forName:#"testTransformer"];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = #[#{#"name":#"William", #"age":#"24"},#{#"name":#"Thomas", #"age":#"23"},#{#"name":#"Alexander", #"age":#"64"},#{#"name":#"James", #"age":#"47"}];
}
In the RDTransformer class is this:
+ (Class)transformedValueClass {
return [NSString class];
}
+(BOOL)allowsReverseTransformation {
return NO;
}
-(id)transformedValue:(id)value {
NSLog(#"%#",value);
return value;
}
In IB, I added an NSPopupButton to the window and an array controller to the objects list. The content array of the controller is bound to App Delegate.theData, and the Content Values of the popup button is bound to Array Controller.arrangedObjects.name with the value transformer, testTransformer.
When I run the program, the log from the transformedValue: method is this:
2012-09-19 20:31:39.975 PopupBindingWithTransformer[793:303] (
)
2012-09-19 20:31:40.019 PopupBindingWithTransformer[793:303] (
William,
Thomas,
Alexander,
James
)
This doesn't seem to be other people's experience from what I can see on SO. Is there something I'm doing wrong with either the bindings or the value transformer?

Unfortunately, this is how NSPopUpButton works. The problem is not limited to that control. If you try binding an NSArrayController.contentArray to another NSArrayControllers.arrangedObject.someProperty you will get the same problem. Here is a simple workaround that I use in all my value transformers, which makes them work with both tables and popups:
You can modify your value transformer in the following way:
-(id)transformedArrayValue:(NSArray*)array
{
NSMutableArray *result = [NSMutableArray array];
for (id value in array)
[result addObject:[self transformedValue:value]];
return result;
}
-(id)transformedValue:(id)value
{
if ([value isKindOfClass:[NSArray class]])
return [self transformedArrayValue:value];
// Do your normal-case transform...
return [value lowercaseString];
}
It's not perfect but it's easy to replicate. I actually put the transformedArrayValue: in a class category so that I don't need to copy it everywhere.

Related

NSOutlineView shows only first 2 levels of the NSTreeController hierarchy

I'm populating a NSOUtlineView with a NSTreeController.
The NSTreeController is a 3 levels hierarchy controller (CBMovie, CBDisc and CBEpisode), but only the first 2 levels are displayed in the outline view.
The implementation is the same for all objects: I've implemented methods to specify children, children count and if the object is a leaf. These methods are correctly called for all objects (also for those ones that are not displayed, the grandchildren: CBEpisode).
In the outline View, everything is displayed correctly for the first 2 level. But grandchildren are never displayed, I don't have the option to expand their parent to see them. I can only see CBMovie and CBDiscs.
I'm wondering if there is another setting I'm missing, about how deep the nodes can expand in NSTreeControllers or NSOutlineView configurations.
Below: Implementation in one of the three nodes.
Each node class has different path to its children. This is specified in the -(NSArray*)children method (correctly called).
-(NSArray*)children
{
return [[self Episodes] allObjects];
}
-(int)childrenCount
{
return [[self Episodes] count];
}
-(BOOL)isLeaf
{
return ![[self Episodes] count];
}
Output of logging code. The datasource, the NSTreeController, seems to have the correct structure.
CBMovie
CBDisc
CBEpisode
CBEpisode
CBMovie
CBDisc
CBDisc
CBDisc
CBDisc
CBMovie
CBDisc
CBEpisode
CBEpisode
This is how I populate the NSOutlineView (cell based). I don't use datasource methods, but I'm binding it programmatically.
NSMutableDictionary *bindingOptions = [[NSMutableDictionary alloc] initWithCapacity:2];
if (metadata.valueTransformer) {
[bindingOptions setObject:metadata.valueTransformer forKey:NSValueTransformerNameBindingOption];
}
[bindingOptions setObject:[NSNumber numberWithBool:NO] forKey:NSCreatesSortDescriptorBindingOption];
[bindingOptions setObject:[NSNumber numberWithBool:NO] forKey:NSRaisesForNotApplicableKeysBindingOption];
[newColumn bind:#"value" toObject:currentItemsArrayController withKeyPath:[NSString stringWithFormat:#"arrangedObjects.%#", metadata.columnBindingKeyPath] options:bindingOptions];
Examining the NSTreeController side
The NSTreeController is a 3 levels hierarchy controller, but only the first 2 levels are displayed in the outline view.
Have you confirmed that all three levels are loaded into your NSTreeController? You could do this by logging its contents to the console with the extension below. If the output produced by this code matches what appears in your outline-view, the problem is probably on the NSTreeController side, not the outline-view.
#import "NSTreeController+Logging.h"
#implementation NSTreeController (Logging)
// Make sure this is declared in the associated header file.
-(void)logWithBlock:(NSString * (^)(NSTreeNode *))block {
NSArray *topNodes = [self.arrangedObjects childNodes];
[self logNodes:topNodes withIndent:#"" usingBlock:block];
}
// For internal use only.
-(void)logNodes:(NSArray *)nodes
withIndent:(NSString *)indent
usingBlock:(NSString * (^)(NSTreeNode *))block {
for (NSTreeNode * each in nodes) {
NSLog(#"%#%#", indent, block(each));
NSString *newIndent = [NSString stringWithFormat:#" %#", indent];
[self logNodes:each.childNodes withIndent:newIndent usingBlock:block];
}
}
#end
The above code does not need to be adjusted, all you need to do is call it with a customised block:
-(void)logIt {
[self.treeController logWithBlock:^NSString *(NSTreeNode * node) {
// This will be called for every node in the tree. This implementation
// will see the object's description logged to the console - you may
// want to do something more elaborate.
NSString *description = [[node representedObject] description];
return description;
}];
}
Examining the NSOutlineView side
If all the data seems to have loaded correctly into the NSTreeController you could have a look at how your NSOutlineView is populated. The delegate method -[NSOutlineViewDelegate outlineView:viewForTableColumn:item] is a good place to start. The item argument is the NSTreeNode instance, so, before you return the relevant view you can look at this node and make sure it's behaving as expected. In your case, you should scrutinize the properties of item objects that are representing CBDisc objects. (You may need to click on disclosure buttons to get this method to fire for the relevant objects.)
-(NSView *)outlineView:(NSOutlineView *)outlineView
viewForTableColumn:(NSTableColumn *)tableColumn
item:(id)item {
NSTreeNode *node = (NSTreeNode *)item;
NSManagedObject *representedObject = (NSManagedObject *)node.representedObject;
// Query the node
NSLog(#"%# <%#>", representedObject.description, [representedObject class]);
NSLog(#" node is a leafNode: %#", node.isLeaf ? #"YES" : #"NO");
NSLog(#" node has child-count of: %lu", (unsigned long)node.childNodes.count);
// Query the NSManagedObject
// your stuff here...
// This is app-specific - you'll probably need to change the identifier.
return [outlineView makeViewWithIdentifier:#"StandardTableCellView" owner:self];
}
So, I've figured out why.
Quite stupid: the main outline column containing the disclosure arrows was 20px width only and the arrows of the children had indentation.
I'm using the outline column for the arrows only and not the titles of the nodes, that's why it's so narrow.
I had disabled indentation and now I can see all arrows.

Enable a button based on a value in the NSTableview

I have a NSTableview. I need to enable the button based on a value of a column in the tableview. For instance, In the table view i have a column, Status. I have 2 kinds of status, Withdrawn and Booked. If i click on a row which has the status as Withdrawn, i need to disable the withdraw button.
Can i be able to do it through binding? How could i do it? Pls help me out. Thanks.
Provided you create a custom NSValueTransformer, you can enable or disable the button using bindings.
You can bind the Enabled property of the button as follows:
Bind to: arrayController
Controller Key: selection
Model Key Path: status
Value Transformer: MDStatusValueTransformer
NOTE: in place of arrayController, you should select whatever the name of your array controller is in the nib file. In place of MDStatusValueTransformer, you should specify whatever class name you end up naming the class I've provided below.
As I mentioned, you'll need to create a custom NSValueTransformer. The enabled property expects a BOOL wrapped in an NSNumber, but your status property is an NSString. So, you'll create a custom NSValueTransformer that will examine the incoming status NSString, and return NO if status is equal to #"Withdrawn".
The custom NSValueTransformer should look something like this:
MDStatusValueTransformer.h:
#interface MDStatusValueTransformer : NSValueTransformer
#end
MDStatusValueTransformer.m:
#implementation MDStatusValueTransformer
+ (Class)transformedValueClass {
return [NSNumber class];
}
+ (BOOL)allowsReverseTransformation {
return NO;
}
- (id)transformedValue:(id)value {
if (value == nil) return nil;
if (![value isKindOfClass:[NSString class]]) return nil;
if ([value isEqualToString:#"Withdrawn"]) {
return [NSNumber numberWithBool:NO];
}
return [NSNumber numberWithBool:YES];
}
#end

Accessing the contents of an array Objective C

I have a json structure like this
I am trying to check for the "type" in each nested widgets array.So if it is of a certain type then i am trying to extract properties like fade steps,width, height etc.
Is there a way of doing it using the for loop?
Right now I am doing like this:
for (NSString *widgetArray in widgets)
{
NSLog(#"%#", widgetArray);
}
Its printing the contents, How do I extract corresponding values?
for(NSDictionary *asset in [assets allValues])
{
NSString *type = asset[#"type"];
if ([type isEqualToString:#"gradient"])
{
[gradients addObject:asset];
}
This is the pseudo code provided by one of the members which helped me access the contents of the dictionary, I am not able to apply a similar logic to the nested array structure.
Since the problem is recursive search, you have to use stack. Here are the steps:
Create NSMutableArray that will serve as the stack. Now it is empty.
Take the root widgets array and put all its objects onto the stack (-addObjectsFromArray:).
Take out the first widget (NSDictionary) in stack. Be sure to take it out of the stack (-removeObjectAtIndex:) after storing it in variable.
Do your type comparision and other stuff with that variable.
Take its subwidgets and put all of them onto the stack just like in step 2.
If the stack has something in it, repeat from step 3. Otherwise you processed all widgets.
Pseudo code:
// 1
NSMutableArray *stack = [NSMutableArray array];
// 2
[stack addObjectsFromArray:rootWidgets];
while (stack.count > 0) {
// 3
NSDictionary *widgetDict = [stack objectAtIndex:0];
[stack removeObjectAtIndex:0];
// 4
if ([[widgetDict objectForKey:#"type"] isEqualToString:...]) // Do whatever...
// 5
[stack addObjectsFromArray:[widgetDict objectForKey:#"widgets"]];
}
// 6
// You are done...
To check the type of each element, use the NSObject isKindOfClass: method.
So, you might have something like:
if ([obj isKindOfClass:[NSDictionary class]]) { ... }
Or
if ([obj isKindOfClass:[NSArray class]]) { ... }
Like #H2CO3 says, you use objectAtIndex: to access array elements and objectForKey: to access dictionary elements.
EDIT: I just realized that by "of a certain type" the OP meant the value of the "type" field, rather than the objective-c class of an entry. Still, it is often useful when reading JSON data to determine whether an entry is an array or dictionary, so I will leave my answer here.
In the latest objective C you can use subscripting like C arrays. For example,
NSLog(#"%#", widgetArray[i]);
For dictionaries you can extract via key:
NSString* id = myDict[#"id"];

Populate and bind an NSTableView to multiple array controllers

I have an API provided NSArray with a bunch of content objects – we'll call this acquiredFruit – and an empty NSMutableArray called likedFruit.
I've created NSArrayControllers for both arrays and bound my TableView to acquiredFruit.arrangedObjects. The first column of the tableView is bound to arrangedObjects.name and correctly shows all the delicious fruit.
I've created a second column with a checkbox – when the user fills the box I'd like to add the fruit to my likedFruit array. Unchecking the box should remove the fruit object from the likedFruit array.
Essentially I'd like my NSTableView to join between two array controllers. I have a feeling I should be making a single separate controller for this, but I'm unsure how to approach the problem.
I should also mention that I'm aware I could iterate through my array and construct another object with the fields I need, but my goal is to do this by using bindings, if possible.
Thoughts?
I think you should use one array controller.
You can have an attribute on Fruit called liked. Now your "liked" checkbox column is connected to arrangedObjects.liked. Later, when you want to determine the set of all liked fruits, you can query your fruits array:
NSArray * likedFruits = [ allFruitsArray filteredArrayUsingPredicate:[ NSPredicate predicateWithFormat:#"liked = YES"] ] ;
If in another part of your UI you are displaying only liked fruit, you can set your array controller's filterPredicate to the predicate above to get just those fruits.
EDIT: Let's say NSFruit is provided via someone else's API. Let's use the "General Technique for Adding Properties to Someone Else's Class":
#interface NSFruit (Liking)
#property ( nonatomic ) BOOL liked ;
#end
#implementation NSFruit (Liking)
-(BOOL)liked
{
return [ objc_getAssociatedObject( self, "_abliked" ) boolValue ] ;
}
-(void)setLiked:(BOOL)b
{
objc_setAssociatedObject( self, "_abliked", [ NSNumber numberWithBool:b ], OBJC_ASSOCIATION_RETAIN_NONATOMIC ) ;
}
#end
(I've written this same code for like 100 posts recently!)
I'm not at my Xcode computer right now, so i can't test this, but it seems like you don't really need another array controller, but just another array to hold the likedFruits. I think you need to create an array of dictionaries from your acquiredFruits array that would have one key for the fruit name and another key with a bool value for whether the check box is checked --this bool would be bound to your second column. I'm not sure about the next step on how to tell the likedFruit array that it need to add a new fruit -- I think the check box could have an action method that you could use to have the likedFruit array add the object in the row where the check box was clicked.
After Edit:
Here is an example of how to do what I suggested. I take an array of fruits and turn it into an array of dictionaries (called theData) that include a key for the value of your check box (In IB the content array of the array controller is bound to theData, and the columns are bound to Array Controller.arrangedObjects.fruitName and Array Controller.arrangedObjects.isLiked). checkChanged is an IBAction connected to the check box (but note the sender is actually the table view), and I use the value of the check box to determine whether to add a fruit to likedFruits or delete one. I put one more method, connected to a button just to check the values in likedFruits.
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = [NSMutableArray array];
self.likedFruit =[NSMutableArray array];
NSArray *acquiredFruits = #[#"Apple",#"Orange",#"Pear",#"Peach"];
for (NSString *aFruit in acquiredFruits) {
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:aFruit,#"fruitName",[NSNumber numberWithBool:NO],#"isLiked", nil];
[self.theData addObject:[dict mutableCopy]];
}
self.theData = _theData;
// NSLog(#"%#",self.theData);
}
-(IBAction)checkChanged:(NSTableView *)sender { //connected to the button cell in the table view (but sender is the table view)
NSString *theFruit = [[self.controller.arrangedObjects objectAtIndex:sender.clickedRow ] valueForKey:#"fruitName"];
BOOL doWeLikeIt = [[[self.controller.arrangedObjects objectAtIndex:sender.clickedRow] valueForKey:#"isLiked"] boolValue];
if (doWeLikeIt) {
[self.likedFruit addObject:theFruit];
}else{
[self.likedFruit removeObject:theFruit];
}
}
-(IBAction)logLikedFruits:(id)sender {
NSLog(#"%#",self.likedFruit);
}

Outlets not working after showing "old" view again

I have several UIViews in my Storyboard and, of course, I can switch between them using a segue. Initially this works just fine:
notenKurse is a NSMutableArray, and kurse1Outlets is an outlet collection with my UITextFields.
int counter = 0;
for (UITextField *tf in kurse1Outlets) {
NSMutableString *t = [NSMutableString stringWithFormat:#"%#", [notenKurse objectAtIndex:counter]];
NSLog(#"Object at index %i is %#", counter, [notenKurse objectAtIndex:counter]);
if ([t isEqualToString:#"42"]) {
[t setString:#""];
}
[tf setText:t];
NSLog(#"UITextField in slot %i should now display %#", counter, t);
counter++;
}
All of my UITextFields are displaying the value stored in the array. But if I go to another view (let's assume I have a Button for it ;) ) Change something, and then go back to the original UIView the above code still gets executed, and there are different values in the array (this is supposed to be). I can see that in the log. But the stupid UITextField just doesn't display anything. Neither what was in there before, nor the new text. But why? The log clearly shows that t is what it's supposed to be, so the error must be in writing it into the textfield, and therefore I guess it's an outlet issue...
There is no guarantee of the order of your outlet collection. It's treated very much like an NSDictionary as opposed to an NSArray - where order is guaranteed. Iterating over this sort of collection will yield different results for different devices/people/phase of the moon.
When I use a collection like this I tend to set the 'tag' and then reorder the outlet collection when viewDidLoad by sorting off of the tag.
self.calendarDayImageViews = [_calendarDayImageViews sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
if ([(UIView *)obj1 tag] < [(UIView *)obj2 tag]) {
return NSOrderedAscending;
}
else if([(UIView *)obj1 tag] > [(UIView *)obj2 tag]){
return NSOrderedDescending;
}
else{
return NSOrderedSame;
}
}];
You can just output the tf,by
NSLog(#"%#",tf);
To check if the tf is null
Ok, i found it. I forgot to release some stuff, and so my UITextFields did get set before the array was sorted. My mistake!