NSMenu -- deleting items from menu while looping over items in menu - objective-c

Can I write code like the following? It seems to work, but I want to make sure that it is allowed/safe:
// menu is a NSMenu*
for (NSMenuItem *item in [menu itemArray]) {
if (some condition) {
[menu removeItem:item];
}
}

If it doesn't immediately throw an exception (as modifying an array directly when enumerating it does) then it's probably okay. NSMenu may be giving you a copy of its internal array (assuming it's keeping its items in an NSArray).
For guaranteed safety, however, you might like to do this in two steps. Create another array to hold the items for removal, then enumerate that and do the removal:
NSMutableArray * itemsToRemove = [NSMutableArray array];
for( NSMenuItem *item in [menu itemArray] ){
if( some condition ){
[itemsToRemove addObject:item];
}
}
for( NSMenuItem * item in itemsToRemove ){
[menu removeItem:item];
}

Just for the record:
if you care about memory and dont want to create a secondary array use this:
for (int i = menu.itemArray.count - 1; i >= 0; --i)
{
if( some condition for index i ){
[menu removeItemAtIndex:i];
}
}

Related

NSMutableArray was mutated while being enumerated

I have an array in an old objective-C app that I am using to learn more "complicated" coding. It is back from the old days of OS X and was very much broken. I have gotten it to work (mostly)! However, the app has an NSMutableArray of images, 7 in total. I use a random number generator to insert the images on the screen, some code to allow them to fall, and then, using screen bounds, when they reach "0" on the Y axis they are removed from the array.
I initially just had:
if( currentFrame.origin.y+currentFrame.size.height <= 0 )
{
[flakesArray removeObject:myItem];
I have read when removing objects from an array it is best practice to iterate in reverse...so I have this bit of code:
for (NSInteger i = myArray.count - 1; i >= 0; i--)
{ //added for for statement
if( currentFrame.origin.y+currentFrame.size.height <= 0 )
{
[myArray removeObjectAtIndex:i];
}
Sadly both methods result in the same mutated while enumerated error. Am I missing something obvious?
If I add an NSLog statement I can get, I think, the index of the item that needs to be removed:
NSLog (#"Shazam! %ld", (long)i);
2017-01-07 14:39:42.086667 MyApp[45995:7500033] Shazam! 2
I have looked through a lot and tried several different methods including this one, which looks to be the most popular with the same error.
Thank you in advance! I will happily provide any additional information!
Adding more:
Sorry guys I am not explicitly calling NSFastEnumeration but I have this:
- (void) drawRectCocoa:(NSRect)rect
{
NSEnumerator* flakesEnum = [flakesArray objectEnumerator];
then
for( i = 0; i < numberToCreate; i++ )
{
[self newObject:self];
}
while( oneFlake = [flakesEnum nextObject] )
It is here where:
if( currentFrame.origin.y+currentFrame.size.height <= 0 )
{
NSLog (#"Shazam! %i", oneFlake);
[flakesArray removeObject:oneFlake];
}
Thank you all. I am learning a lot from this discussion!
There are two ways to go: (1) collect the objects to remove then remove them with removeObjectsInArray:.
NSMutableArray *removeThese = [NSMutableArray array];
for (id item in myArray) {
if (/* item satisfies some condition for removal */) {
[removeThese addObject:item];
}
}
// the following (and any other method that mutates the array) must be done
// *outside of* the loop that enumerates the array
[myArray removeObjectsInArray:removeThese];
Alternatively, reverseObjectEnumeration is tolerant of removes during iteration...
for (id item in [myArray reverseObjectEnumerator]) {
if (/* item satisfies some condition for removal */) {
[myArray removeObject: item];
}
}
As per the error, you may not mutate any NSMutableArray (or any NSMutable... collection) while it is being enumerated as part of any fast enumeration loop (for (... in ...) { ... }).
#danh's answer works as well, but involves allocating a new array of elements. There are two simpler and more efficient ways to filter an array:
[array filterUsingPredicate:[NSPredicate predicateWithBlock:^(id element, NSDictionary<NSString *,id> *bindings) {
// if element should stay, return YES; if it should be removed, return NO
}];
or
NSMutableIndexSet *indicesToRemove = [NSMutableIndexSet new];
for (NSUInteger i = 0; i < array.count; i += 1) {
if (/* array[i] should be removed */) {
[indicesToRemove addIndex:i];
}
}
[array removeObjectsAtIndexes:indicesToRemove];
filterUsingPredicate: will likely be slightly faster (since it uses fast enumeration itself), but depending on the specific application, removeObjectsAtIndexes: may be more flexible.
No matter what, if you're using your array inside a fast enumeration loop, you will have to perform the modification outside of the loop. You can use filterUsingPredicate: to replace the loop altogether, or you can keep the loop and keep track of the indices of the elements you want to remove for later.

Extend selection in NSCollectionView with Shift key

I recently reviewed one of my Applications that I released a year ago.
And I see that nowadays the NSCollectionView inside it has lost the selection functioning such as SHIFT + Select now it behaving as CMD + Select.
(Secondary issue: I am also not getting a selection rectangle when dragging with the mouse.)
Obviously I want this feature back, where using shift would expand the selection from the previously clicked cell to the shift-clicked cell.
What have I done:
//NSCollectionView * _picturesGridView; //is my iVar
//In initialization I have set my _picturesGridView as follows
//Initializations etc are omitted -- (only the selection related code is here)
[_picturesGridView setSelectable:YES];
[_picturesGridView setAllowsMultipleSelection:YES];
Question: Is there an easy way to get back this functionality? I don't see anything related in documentation and I couldn't find any solution on the internet.
Sub Question: If there is no easy way to achieve that -> Should I go ahead and create my own FancyPrefix##CollectionViewClass and to reimplement this feature as I wish -- Or is it better to go over the existing NSCollectionView and force it to behave as I wish?
Sub Note: Well if I will find myself reimplementing it it will be light weight class that will just comply to my own needs -- I mean I will not mimic the entire NSCollectionView class.
P.S.
I am able to select an item by clicking on it I am able to select multiple items only with CMD+Click or SHIFT+Click but the latter behaves exactly as CMD+Click which I don't want as well.
As for the mouse Selection Rectangle - I didn't override any Mouse events. It is not clear why I don't have this functionality.
Per Apple's documentation, shouldSelectItemsAtIndexPaths: is the event that only gets invoked when the user interacts with the view, but not when the selection is modified by one's own code. Therefore, this approach avoids side effects that may occur when using didSelectItemsAt:.
It works as follows:
It needs to remember the previously clicked-on item. This code assumes that the NSCollectionView is actually a subclass called CustomCollectionView that has a property #property (strong) NSIndexPath * _Nullable lastClickedIndexPath;
It only remembers the last clicked-on item when it was a simple selection click, but not, for instance, a drag-selection with more than one selected item.
If it detects a second single-selection click with the Shift key down, it selects all the items in the range from the last click to the current click.
If an item gets deselected, we clear the last clicked-on item in order to simulate better the intended behavior.
- (NSSet<NSIndexPath *> *)collectionView:(CustomCollectionView *)collectionView shouldDeselectItemsAtIndexPaths:(NSSet<NSIndexPath*> *)indexPaths
{
collectionView.lastClickedIndexPath = nil;
return indexPaths;
}
- (NSSet<NSIndexPath *> *)collectionView:(CustomCollectionView *)collectionView shouldSelectItemsAtIndexPaths:(NSSet<NSIndexPath*> *)indexPaths
{
if (indexPaths.count != 1) {
// If it's not a single cell selection, then we also don't want to remember the last click position
collectionView.lastClickedIndexPath = nil;
return indexPaths;
}
NSIndexPath *clickedIndexPath = indexPaths.anyObject; // now there is only one selected item
NSIndexPath *prevIndexPath = collectionView.lastClickedIndexPath;
collectionView.lastClickedIndexPath = clickedIndexPath; // remember last click
BOOL shiftKeyDown = (NSEvent.modifierFlags & NSEventModifierFlagShift) != 0;
if (NOT shiftKeyDown || prevIndexPath == nil) {
// not an extension click
return indexPaths;
}
NSMutableSet<NSIndexPath*> *newIndexPaths = [NSMutableSet set];
NSInteger startIndex = [prevIndexPath indexAtPosition:1];
NSInteger endIndex = [clickedIndexPath indexAtPosition:1];
if (startIndex > endIndex) {
// swap start and end position so that we can always count upwards
NSInteger tmp = endIndex;
endIndex = startIndex;
startIndex = tmp;
}
for (NSInteger index = startIndex; index <= endIndex; ++index) {
NSUInteger path[2] = {0, index};
[newIndexPaths addObject:[NSIndexPath indexPathWithIndexes:path length:2]];
}
return newIndexPaths;
}
This answer has been made "Community wiki" so that anyone may improve this code. Please do so if you find a bug or can make it behave better.
I hacked together a sample of doing this, starting with the sample code git#github.com:appcoda/PhotosApp.git, all my changes are in ViewController.swift.
I kept track of whether the shift was up or down by adding
var shiftDown = false
override func flagsChanged(with event: NSEvent) {
shiftDown = ((event.modifierFlags.rawValue >> 17) & 1) != 0
}
I added this instance variable to remember the last selection
var lastIdx: IndexPath.Element?
Then, to the already defined collectionView(_:didSelectItemsAt), I added
let thisIdx = indexPaths.first?[1]
if shiftDown, let thisIdx = thisIdx, let lastIdx = lastIdx {
let minItem = min(thisIdx, lastIdx)
let maxItem = max(thisIdx, lastIdx)
var additionalPaths = Set<IndexPath>()
for i in minItem...maxItem {
additionalPaths.insert(IndexPath(item: i, section: 0))
}
collectionView.selectItems(at: additionalPaths, scrollPosition: [])
}
lastIdx = thisIdx
This is just a start. There's probably bugs, especially around the saving of the lastIdx.
None of the answers appear to support sections or shift selecting across section boundaries. Here is my approach:
- (NSSet<NSIndexPath *> *)collectionView:(NSCollectionView *)collectionView shouldSelectItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths
{
if ([shotCollectionView shiftIsDown])
{
// find the earliest and latest index path and make a set containing all inclusive indices
NSMutableSet* inclusiveSet = [NSMutableSet new];
__block NSIndexPath* earliestSelection = [NSIndexPath indexPathForItem:NSUIntegerMax inSection:NSUIntegerMax];
__block NSIndexPath* latestSelection = [NSIndexPath indexPathForItem:0 inSection:0];
[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, BOOL * _Nonnull stop) {
NSComparisonResult earlyCompare = [obj compare:earliestSelection];
NSComparisonResult latestCompare = [obj compare:latestSelection];
if (earlyCompare == NSOrderedAscending)
{
earliestSelection = obj;
}
if (latestCompare == NSOrderedDescending)
{
latestSelection = obj;
}
}];
NSSet<NSIndexPath *>* currentSelectionPaths = [shotCollectionView selectionIndexPaths];
[currentSelectionPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, BOOL * _Nonnull stop) {
NSComparisonResult earlyCompare = [obj compare:earliestSelection];
NSComparisonResult latestCompare = [obj compare:latestSelection];
if (earlyCompare == NSOrderedAscending)
{
earliestSelection = obj;
}
if (latestCompare == NSOrderedDescending)
{
latestSelection = obj;
}
}];
NSUInteger earliestSection = [earliestSelection section];
NSUInteger earliestItem = [earliestSelection item];
NSUInteger latestSection = [latestSelection section];
NSUInteger latestItem = [latestSelection item];
for (NSUInteger section = earliestSection; section <= latestSection; section++)
{
NSUInteger sectionMin = (section == earliestSection) ? earliestItem : 0;
NSUInteger sectionMax = (section == latestSection) ? latestItem : [self collectionView:collectionView numberOfItemsInSection:section];
for (NSUInteger item = sectionMin; item <= sectionMax; item++)
{
NSIndexPath* path = [NSIndexPath indexPathForItem:item inSection:section];
[inclusiveSet addObject:path];
}
}
return inclusiveSet;
}
// Otherwise just pass through
return indexPaths;
}

How to remove all items from NSCollectionView?

I am trying to remove all the items that user has added in NSCollectionView by removing all items from NSMutableArray. It's not working.
I just need to Refresh/Reload by Clear/Remove all items of the NSCollectionView
Try this
NSIndexSet* selectedIndexSet = [_lablesCollectionView selectionIndexes];
NSUInteger index = [selectedIndexSet firstIndex] ;
NSLog(#"index is %d", index);
[lablesArray removeObjectsAtArrangedObjectIndexes:selectedIndexSet];
Hope it helps.
//remove/clear all items from NSCollectionView
for (int i = 0; i < [[arrayController arrangedObjects] count]; i++) {
[arrayController removeObjectAtArrangedObjectIndex:i];
i--;
}

How to safely remove object from a nsmutablearray while iterating through it?

I am using a nsmutablearray in loop and want to remove its object (or assign nil) that has just traversed.
But if I am doing so, I get an error as <__NSArrayM: 0x8c3d3a0> was mutated while being enumerated.' . The code is as below
- (TreeNode*)depthLimitedSearch:(TreeNode *)current costLimit:(int)currentCostBound {
NSMutableArray *children=[NSMutableArray arrayWithArray:[current expandNodeToChildren]];
for (TreeNode *s in children) {
if (s.puzzleBox.isFinalPuzzleBox) {//checking for final puzzleBox
return s;
}
/*exploredNodes++;
if (exploredNodes %10000==0) {
NSLog(#"explored nodes for this treshold-%d are %d",currentCostBound,exploredNodes);
}*/
int currentCost =[s.cost intValue]+[s.heuristicsCost intValue];
if (currentCost <= currentCostBound) {
//[s.puzzleBox displayPuzzleBox];
TreeNode *solution = [self depthLimitedSearch:s costLimit:currentCostBound];
if (solution!=nil){//&& (bestSolution ==nil|| [solution.cost intValue] < [bestSolution.cost intValue])) {
bestSolution = solution;
return bestSolution;
}
}else {
if (currentCost < newLimit) {
//NSLog(#"new limit %d", currentCost);
newLimit = currentCost;
}
}
// here I want to free memory used by current child in children
[children removeObject:s]
}
children=nil;
return nil;
}
and I have commented the place where I want to release the space used by the child.
You should not be using a for...in loop if you want to remove elements in the array. Instead, you should use a normal for loop and go backwards in order to make sure you don't skip any items.
for (NSInteger i = items.count - 1; i >= 0; i--) {
if (someCondition) {
[items removeObjectAtIndex:i];
}
}
You can collect the items to be removed in another array and remove them in a single pass afterwards:
NSMutableArray *toRemove = [NSMutableArray array];
for (id candidate in items) {
if (something) {
[toRemove addObject:candidate];
}
}
[items removeObjectsInArray:toRemove];
It’s easier than iterating over indexes by hand, which just asking for off-by-one errors.
Not sure how this plays with your early returns, though.

fast enumeration for removing item in NSMutableArray crash

i have a strange issue , if i remove my item at forin enumeration , it would crash , so like this:
for (Obstacle *obstacleToTrack in _obstaclesToAnimate) {
//this if else not so important for happening crash
if(obstacleToTrack.distance > 0){
obstacleToTrack.distance -= _playerSpeed * _elapsed;
}else{
if (obstacleToTrack.watchOut) {
obstacleToTrack.watchOut = NO;
}
obstacleToTrack.x -= (_playerSpeed + obstacleToTrack.speed) * _elapsed;
}
if (obstacleToTrack.x < -obstacleToTrack.width || _gameState == GS_OVER) {
[self removeChild:obstacleToTrack];
//this line makes crash happen , if remove this line code work fine
[_obstaclesToAnimate removeObject:obstacleToTrack];
}
}
if i change my code to
NSMutableArray *forRemoving = [[NSMutableArray alloc]init];
for (Obstacle *obstacleToTrack in _obstaclesToAnimate) {
//this if else not so important for happening crash
if(obstacleToTrack.distance > 0){
obstacleToTrack.distance -= _playerSpeed * _elapsed;
}else{
if (obstacleToTrack.watchOut) {
obstacleToTrack.watchOut = NO;
}
obstacleToTrack.x -= (_playerSpeed + obstacleToTrack.speed) * _elapsed;
}
if (obstacleToTrack.x < -obstacleToTrack.width || _gameState == GS_OVER) {
// code change here
[self removeChild:obstacleToTrack];
[forRemoving addObject:obstacleToTrack];
}
}
for(Obstacle *obstacleToTrack in forRemoving){
[_obstaclesToAnimate removeObject:obstacleToTrack];
[forRemoving removeObject:obstacleToTrack];
}
[forRemoving release];
this would work perfect , could someone tell me why?
The answer is that if you remove an object the other objects in that array move postion in the array since an item is removed.
For example we have an array with 4 items, if we remove the first item (item 0) the item that used to be at index 1 is now at index 0 and the item at 2 is now at 1.
Thus the enumeration breaks.
You could solve this by looping thru the array from the count down to 0:
for (int i = [array count]-1; i >= 0; i--) {
id object = [array objectAtIndex:i];
if (some check) {
[array removeObjectAtIndex:i];
}
}
Like rckoenes said, you break the enumaration by removing stuff in the array while iterating through it.
What you can do is to have a second array where you insert the objects that you want to remove. Then, after your enumeration is finished you can remove all the objects that are found in the second array, from your first array.
You must not modify a collection while iterating through its items.
If you iterate index based (i.e. the classical for loop), you can remove things, but be careful about adjusting your index.
Check this link
https://developer.apple.com/library/mac/documentation/cocoa/conceptual/Collections/Articles/Enumerators.html
Enumeration is “safe”—the enumerator has a mutation guard so that if
you attempt to modify the collection during enumeration, an exception
is raised.