What do I need to do to update a tableView bound to an NSArrayController when a method is called that updates the underlying array? An example might clarify this.
When my application launches, it creates a SubwayTrain. When SubwayTrain is initialised, it creates a single SubwayCar. SubwayCar has a mutable array 'passengers'. When a Subway car is initialised, the passengers array is created, and a couple of People objects are put in (let's say a person with name "ticket collector" and another, named "homeless guy"). These guys are always on the SubwayCar so I create them at initialisation and add them to the passengers array.
During the life of the application people board the car. 'addPassenger' is called on the SubwayCar, with the person passed in as an argument.
I have an NSArrayController bound to subwayTrain.subwayCar.passengers, and at launch my ticket collector and homeless guy show up fine. But when I use [subwayCar addPassenger:], the tableView doesn't update. I have confirmed that the passenger is definitely added to the array, but nothing gets updated in the gui.
What am I likely to be doing wrong? My instinct is that it's KVO related - the array controller doesn't know to update when addPassenger is called (even though addPassenger calls [passengers addObject:]. What could I be getting wrong here - I can post code if it helps.
Thanks to anyone willing to help out.
UPDATE
So, it turns out I can get this to work by changing by addPassenger method from
[seatedPlayers addObject:person];
to
NSMutableSet *newSeatedPlayers = [NSMutableSet setWithSet:seatedPlayers];
[newSeatedPlayers addObject:sp];
[seatedPlayers release];
[self setSeatedPlayers:newSeatedPlayers];
I guess this is because I am using [self setSeatedPlayers]. Is this the right way to do it? It seems awfully cumbersome to copy the array, release the old one, and update the copy (as opposed to just adding to the existing array).
I don't know if its considered a bug, but addObject: (and removeObject:atIndex:) don't generate KVO notifications, which is why the array controller/table view isn't getting updated. To be KVO-compliant, use mutableArrayValueForKey:
Example:
[[self mutableArrayValueForKey:#"seatedPlayers"] addObject:person];
You'll also want to implement insertObject:inSeatedPlayersAtIndex: since the default KVO methods are really slow (they create a whole new array, add the object to that array, and set the original array to the new array -- very inefficient)
- (void)insertObject:(id)object inSeatedPlayerAtIndex:(int)index
{
[seatedPlayers insertObject:object atIndex:index];
}
Note that this method will also be called when the array controller adds objects, so its also a nice hook for thinks like registering an undo operation, etc.
I haven't tried this, so I cannot say it works, but wouldn't you get KVO notifications by calling
insertObject:atArrangedObjectIndex:
on the ArrayController?
So, it turns out I can get this to work by changing by addPassenger method from
[seatedPlayers addObject:person];
to
NSMutableSet *newSeatedPlayers = [NSMutableSet setWithSet:seatedPlayers];
[newSeatedPlayers addObject:sp];
[seatedPlayers release];
[self setSeatedPlayers:newSeatedPlayers];
I guess this is because I am using [self setSeatedPlayers]. Is this the right way to do it?
First off, it's setSeatedPlayers:, with the colon. That's vitally important in Objective-C.
Using your own setters is the correct way to do it, but you're using the incorrect correct way. It works, but you're still writing more code than you need to.
What you should do is implement set accessors, such as addSeatedPlayersObject:. Then, send yourself that message. This makes adding people a short one-liner:
[self addSeatedPlayersObject:person];
And as long as you follow the KVC-compliant accessor formats, you will get KVO notifications for free, just as you do with setSeatedPlayers:.
The advantages of this over setSeatedPlayers: are:
Your code to mutate the set will be shorter.
Because it's shorter, it will be cleaner.
Using specific set-mutation accessors provides the possibility of specific set-mutation KVO notifications, instead of general the-whole-dang-set-changed notifications.
I also prefer this solution over mutableSetValueForKey:, both for brevity and because it's so easy to misspell the key in that string literal. (Uli Kusterer has a macro to cause a warning when that happens, which is useful when you really do need to talk to KVC or KVO itself.)
The key to the magic of Key Value Observing is in Key Value Compliance. You initially were using a method name addObject: which is only associated with the "unordered accessor pattern" and your property was an indexed property (NSMutableArray). When you changed your property to an unordered property (NSMutableSet) it worked. Consider NSArray or NSMutableArray to be indexed properties and NSSet or NSMutableSet to be unordered properties. You really have to read this section carefully to know what is required to make the magic happen... Key-Value-Compliance. There are some 'Required' methods for the different categories even if you don't plan to use them.
Use willChangeValueForKey: and didChangeValueForKey: wrapped around a change of a member when the change does not appear to cause a KVO notification. This comes in handy when you are directly changing an instance variable.
Use willChangeValueForKey:withSetMutation:usingObjects: and didChangeValueForKey:withSetMutation:usingObjects: wrapped around a change of contents of a collection when the change does not appear to cause a KVO notification.
Use [seatedPlayers setByAddingObject:sp] to make things shorter and to avoid needlessly allocating mutable set.
Overall, I'd do either this:
[self willChangeValueForKey:#"seatedPlayers"
withSetMutation:NSKeyValueUnionSetMutation
usingObjects:sp];
[seatedPlayers addObject:sp];
[self didChangeValueForKey:#"seatedPlayers"
withSetMutation:NSKeyValueUnionSetMutation
usingObjects:sp];
or this:
[self setSeatedPlayers:[seatedPlayers setByAddingObject:sp]];
with the latter alternative causing an automatic invocation of the functions listed under 1. First alternative should be better performing.
Related
I have an entity Category in my Core Data model. Category has a to-many relation to Article. Article has a property read, which is a boolean.
I want to observe the number of unread articles (so I can display it in the title).
A first approach would be something like:
[self.category addObserver:self forKeyPath:#"articles.#sum.read" options:NSKeyValueObservingOptionNew context:nil];
But this doesn't work. I can observe the articles collection to see if something is added, and observe all of the elements individually. I can get this working, but I wonder if there is an easier way. Any hints?
(This might be a duplicate of Using KVO to observe changes to a property on an object inside a collection in Objective-C, but I still think there should be a better way).
How precisely do you need to know what changed and when it changed? If your requirements are very simple, you can listen to the NSManagedObjectContextObjectsDidChangeNotification notification and check the userInfo for NSUpdatedObjectsKey. Those objects will return the changes through their -changedValues method.
How do things get added to the articles collection? It probably easiest to simply hook in there directly in stead of using KVO.
It's a question about good programming techniques with Cocoa.
When you want to call a method on one property of your class, should you use KVC to get the receiver or just put the name of your property?
Example, KVC:
[[self property] myMethod];
Example, simple:
[property myMethod];
Thanks!
Example, KVC:
[[self property] myMethod];
That isn't KVC. The KVC way is:
[[self valueForKey:#"myProperty"] myMethod]
There is no reason to do this when you know the property at compile time; you can just ask for the property value or the ivar value directly. With KVO and (on the Mac) Bindings already implemented, there's not much reason to use KVC directly, as KVO and Bindings use it for you.
Example, simple:
[property myMethod];
That doesn't access the property; it accesses the ivar.
You're only accessing the property when you send an accessor message to the property's holder (self in your examples). It doesn't matter whether you use [self property] or self.property, as they're equivalent; either one is a property message to self, with whatever side effects that implies.
That's the key difference: Hitting the accessor may cause side effects, whereas accessing the ivar directly never will.
Hence, the best practice: Use the property in all your instance methods (as you probably want the accessors' side effects), except in init methods and dealloc, where side effects would be a bad thing. (As a general rule, you should not send messages to a half-initialized or half-deallocked object. The exception is when you explicitly commented the method as being part of your init/dealloc process and therefore wrote it to be safe to use in such circumstances.)
I believe the formal version is technically correct as that will guarantee any side-effects from a funky getter. (To make sure, make a custom getter that includes NSLog("in getter!") and let us know if it works.)
For setting you have to use the [self setProperty:foo]; as property = foo bypasses the setter and can lead to memory leaks.
If it feels more natural to you, the dot notation (e.g., self.property and self.property = foo) is identical to [self property] and [self setProperty:foo].
I'd like to have an NSArray or NSMutableArray that always shows a filtered view of a data set. That is, if the filter is set to "show me things with the letter a", and an item "blah" is added, that item would automatically show up in the list. However, if "bluh" were added, it would not show up in the filtered list (but would still really be there in the underlying dataset).
I see that there are filter methods on NSArray and NSMutableArray, but these are one shot methods. That is, the filter occurs when you call the method and never again (unless of course you call the filter method again).
I'm coming from the Java world, were I used Glazed Lists extensively for this kind of thing. I was hoping for a similar solution baked into Cocoa.
You'll most likely want to use NSArrayController as suggested by Ole.
You can use setFilterPredicate: on it as suggested, and then you'll want to access the objects by calling arrangedObjects on the controller. You don't need setAutomaticallyRearrangesObjects: unless you're intending to have your data sorted (using sort descriptors — NSSortDescriptor instances).
NSArrayController is really set up to handle displaying things in a table view very easily, so if that's your end goal, then this is exactly what you want. It integrates nicely with NSSearchField to handle predicates in the UI.
If you're using this as some back end object that's getting passed around, then you might want to write something yourself that's a little less heavy-weight than NSArrayController.
Coming from the iPhone, I'm not very familiar with NSArrayController but you might want to take a look at it. It seems to me that setFilterPredicate: in combination with setAutomaticallyRearrangesObjects:YES might do the trick.
Other than that, it should be quite easy to roll your own solution using Key-Value Observing. Start with a mutable copy of the array you want to filter and filter it with filterUsingPredicate: as you noted above, then register yourself as an observer for insertions and deletions in the original array and when your observer method gets called, call evaluateWithObject: on the newly inserted objects to decide whether to insert them into your filtered array.
In my app, I have a NSDictionary whose keys should be instances of a subclass of NSManagedObject.
The problem, however, is that NSManagedObject does not implement the NSCopying protocol which means that no Core Data objects / instances of NSManagedObject can be used as dictionary keys even though the -[hash] method works fine for them.
Was should I do?
There are four options:
Use a different object as the dictionary key instead, and lookup from that. [object objectID] or +[NSValue valueWithNonretainedObject:] seem the most obvious
Use CFDictionaryCreateMutable() to create a dictionary with retained keys, rather than copied, instead, and then call CFDictionarySetValue() to store the objects
On OS X or iOS6+, [NSMapTable mapTableWithStrongToStrongObjects] gives you a purely Objective-C equivalent to CFMutableDictionary
Implement NSCopying for your managed object subclass, such that it returns self (with a bumped reference count if you're not using ARC)
Notes
+valueWithNonretainedObject: is pretty dangerous, since it's possible to be left with a dangling pointer; likely best to avoid.
Storing object IDs is fine, apart from the fact that new objects start out life with a temporary ID. That ID then changes to a permanent one when the context is saved to disk (or -obtainPermanentIDsForObjects:… is called). Your mapping code needs to be smart enough to handle this unless it can guarantee that all incoming objects already have a permanent ID.
Implementing NSCopying like this feels a bit icky, but should work just fine. As it happens, this is exactly the approach NSURLSessionTask takes, I presume for dictionary friendliness.
Prior to OS X 10.8 Mountain Lion, it used to be possible to create a regular NSMutableDictionary and then call CFDictionarySetValue() for it. That's no longer the case though; new dictionaries now have proper copy callbacks specified down at the CF level, rather than purely being a feature of NSMutableDictionary.
I suggest to use [[[myManagedObject objectID] URIRepresentation] absoluteString] as your key.
Could you create a wrapper class, that contains a reference to the instance of NSManagedObject that you want to use as a dictionary key? You could then make this wrapper class implement NSCopying, along with a hash method (perhaps just calling the NSManagedObject's hash method), and use this wrapper as the dictionary key.
I had a similar problem, in which I needed to bundle several entities with additional data for each, and initially tried:
#{entity1:data1, #entity2:data2, #entity3:data3}
this didn't work for the reason above (NSCopying), so I did:
#[
#{#"entity":entity1, #"data":data1},
#{#"entity":entity2, #"data":data2},
#{#"entity":entity3, #"data":data3}
]
But this solution makes sense only if you don't need dictionary style access to these entities or are happy to iterate to find what you need. In my case this was a packaging problem. Note that if you pass these entities around the NSManagedObjectContext need to be the same to use them.
I have an object that implements the indexed accessor methods for a key called contents. In those accessors, I call willChange:valuesAtIndexes:forKey: and didChange:valuesAtIndexes:forKey: when I modify the underlying array.
I also have a custom view object that is bound to contents via an NSArrayController. In observeValueForKeyPath:ofObject:change:context: the only value in the change dictionary for the NSKeyValueChangeKindKey I ever see is NSKeyValueChangeSetting. When I'm adding objects to the array, I expect to see NSKeyValueChangeInsertion.
Recreating my view's internal representation of the objects it observes every time I insert a single item -- particularly when I'm bulk loading hundreds of items -- presents quite a performance problem, as you'd imagine. What am I doing wrong that Cocoa seems to think I'm setting a completely new array each time I add or remove a single item?
(Note to all readers: I hate using answers for this, too, but this discussion is too long for comments. The downside, of course, is that it ends up not sorted chronologically. If you don't like it, I suggest you complain to the Stack Overflow admins about comments being length-limited and plain-text-only.)
I don't understand what you mean by implementing array accessors in the view.
Implement accessors, including indexed accessors, for the mutable array property that you've exposed as a binding.
Bindings is built on top of KVO.
And KVC.
All bindings are implemented using observeValueForKeyPath:
Overriding that is one way, sure. The other way is to implement accessors in the object with the bindable property (the view).
My custom view provides a binding that the app binds to an array -- or in this case, an array controller. Accessor methods apply to KVC, not KVO.
Cocoa Bindings will call your view's accessors for you (presumably using KVC). You don't need to implement the KVO observe method (unless, of course, you're using KVO directly).
I know this because I've done it that way. See PRHGradientView in CPU Usage.
Curiously, the documentation doesn't mention this. I'm going to file a documentation bug about it—either I'm doing something fragile or they forgot to mention this very nice feature in the docs.
It absolutely matters that I'm getting a set message on every array update. I wouldn't have posted it as a question if it didn't matter.
There are quite a large number of people who engage in something called “premature optimization”. I have no way of knowing who is one of them without asking.
I have an object that implements the indexed accessor methods for a key called contents. In those accessors, I call willChange:valuesAtIndexes:forKey: and didChange:valuesAtIndexes:forKey: when I modify the underlying array.
Don't do that. KVO posts the notifications for you when you receive a message to one of those accessors.
I also have a custom view object that is bound to contents via an NSArrayController. In observeValueForKeyPath:ofObject:change:context: the only value in the change dictionary for the NSKeyValueChangeKindKey I ever see is NSKeyValueChangeSetting. When I'm adding objects to the array, I expect to see NSKeyValueChangeInsertion.
For one thing, why are you using KVO directly? Use bind:toObject:withKeyPath:options: to bind the view's property to the array controller's arrangedObjects (I assume) property, and implement array accessors (including indexed accessors, if you like) in the view.
For another, remember that arrangedObjects is a derived property. The array controller will filter and sort its content array; the result is arrangedObjects. You could argue that permuting the indexes from the original insertion into a new insertion would be a more accurate translation of the first change into the second, but setting the entire arrangedObjects array was probably simpler to implement (something like [self _setArrangedObjects:[[newArray filteredArrayUsingPredicate:self.filterPredicate] sortedArrayUsingDescriptors:self.sortDescriptors]]).
Does it really matter? Have you profiled and found that your app is slow with wholesale array replacement?
If so, you may need to bind the view directly to the array's content property or to the original array on the underlying object, and suffer the loss of free filtering and sorting.
I call the KVO methods manually for reasons outside the scope of this issue. I have disabled automatic observing for this property. I know what I'm doing there.
I don't understand what you mean by implementing array accessors in the view. Bindings is built on top of KVO. All bindings are implemented using observeValueForKeyPath: My custom view provides a binding that the app binds to an array -- or in this case, an array controller. Accessor methods apply to KVC, not KVO.
It absolutely matters that I'm getting a set message on every array update. I wouldn't have posted it as a question if it didn't matter. I call something like
[[modelObject mutableArrayValueForKey:#"contents"] addObjectsFromArray:hundredsOfObjects];
On every insertion, my view observes a whole new array. Since I'm potentially adding hundreds of objects, that's O(N^2) when it should to be O(N). That is completely unacceptable, performance issues aside. But, since you mention it, the view does have to do a fair amount of work to observe a whole new array, which significantly slows down the program.
I can't give up using an array controller because I need the filtering and sorting, and because there's an NSTableView bound to the same controller. I rely on it to keep the sorting and selections in sync.
I solved my problem with a complete hack. I wrote a separate method that calls the KVO methods manually so that only one KVO message is sent. It's a hack, I don't like it, and it still makes my view reread the entire array -- although only once, now -- but it works for now until I figure out a better solution.