IKImageBrowserView Moving items doesn't animate - objective-c

I have an IKImageBrowser setup which appears to be working well. I have set it up to allow reordering and also set animation to YES (in my awakeFromNib), however whenever I select and try and reorder the images I get strange behaviour:
1) They don't reorder most of the time
2) If they do they don't animate
3) Sometimes the images land on each other, if I scroll away and back they are back where they started.
If I highlight an image and delete it, it animates and disappears as expected...
Is this a problem with Core Animation? Do I need to add a core animation layer to the object in interface builder? I followed this tutorial from Apple to get these results: http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/ImageKitProgrammingGuide/ImageBrowser/ImageBrowser.html#//apple_ref/doc/uid/TP40004907-CH5-SW1
Here's the code in question:
- (BOOL) imageBrowser:(IKImageBrowserView *) aBrowser moveItemsAtIndexes: (NSIndexSet *)indexes toIndex:(NSUInteger)destinationIndex{
int index;
NSMutableArray *temporaryArray;
temporaryArray = [[[NSMutableArray alloc] init] autorelease];
for(index=[indexes lastIndex]; index != NSNotFound;
index = [indexes indexLessThanIndex:index])
{
if (index < destinationIndex)
destinationIndex --;
id obj = [mImages objectAtIndex:index];
[temporaryArray addObject:obj];
[mImages removeObjectAtIndex:index];
}
// Insert at the new destination
int n = [temporaryArray count];
for(index=0; index < n; index++){
[mImages insertObject:[temporaryArray objectAtIndex:index]
atIndex:destinationIndex];
}
return YES;
}
Interestingly, this line throws a warning
for(index=[indexes lastIndex]; index != NSNotFound;
comparison is always true due to
limited range of data type

As mentioned above by NSGod, changing int index to NSUInteger index solved the problem

Related

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;
}

UICollectionView Very Slow Custom Layout

I am using a UICollectionView with a custom layout that lays out cells in a grid format. There can be well over 50 rows and 50 columns. Scrolling occurs both vertically and horizontally. Currently, I am doing all of the layout setup in prepareLayout and storing it in arrays:
- (void)prepareLayout {
NSMutableArray *newLayoutInfo = [[NSMutableArray alloc] init];
NSMutableArray *newLinearLayoutInfor = [[NSMutableArray alloc] init];
NSInteger sectionCount = [self.collectionView numberOfSections];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
self.heightForRows = [delegate collectionViewHeightForAllRows];
self.totalWidthsForRows = [[NSMutableArray alloc] init];
for (int i = 0; i < sectionCount; i++) {
[self.totalWidthsForRows addObject:[NSNumber numberWithInt:0]];
}
for (NSInteger section = 0; section < sectionCount; section++) {
NSMutableArray *cellLayoutInfo = [[NSMutableArray alloc] init];
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForCellAtIndexPath:indexPath];
[cellLayoutInfo addObject:itemAttributes];
[newLinearLayoutInfor addObject:itemAttributes];
}
[newLayoutInfo addObject:cellLayoutInfo];
}
self.layoutInfo = newLayoutInfo;
self.linearLayoutInfo = newLinearLayoutInfor;
}
Then in layoutAttributesForElementsInRect I have:
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *rows = [self.linearLayoutInfo filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *evaluatedObject, NSDictionary *bindings) {
return CGRectIntersectsRect(rect, [evaluatedObject frame]);
}]];
This works okay, but it is laggy and jumpy when I have over 50 columns and 50 rows. The problem I now have is that I must set
-(BOOL)shouldInvalidateLayoutForBoundsChange {
return YES;
}
This makes it prepare the entire layout every time the bounds change, which, needless to say, has a huge impact on performance and you can barely scroll. The cells consist of just text with an opaque background, so there is no issue there.
I am sure I am not doing this right and that there must be a better way. Thanks for the help in advance.
In custom flow layout I do this and it seems to help:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return !(CGSizeEqualToSize(newBounds.size, self.collectionView.frame.size));
}
-(BOOL)shouldInvalidateLayoutForBoundsChange {
return YES;
}
Causes the layout to do prepareLayout() every time it scrolls, which means anything of heavy computing in prepare will lead to a laggy practice, so one possible direction to solve this is to check what's really taking much time. One possibility is what's inside
for (NSInteger section = 0; section < sectionCount; section++)
{
// generate attributes ...
}
in order to generate attributes for the layout. Every time it scrolls, every time this generalization reruns, so that it impacts on the scroll appear to be jumpy and clumsy. So in order to solve this issue, or at least sort out that this is not the really trouble, I suggest setting a flag in this layout algorithm, say, isScrolling, standing for the situation where the layout needs to prepare. Every time in prepareLayout() check the flag, if it is YES, then we'll know there's no need to do for loop to regenerate all the attributes, which alreay exsit ever since the first time the layout is initialised.
ok--I understand now. Here's what I recommend: create 3 collection views... one for the column headers (where each cell is column header), one for the row leaders (each cell = 1 row leader) and one collection view for your cells. Then when the scroll position of any collection view is changed by the user, update the scroll positions for the other 2 collection views as appropriate.

insertItemsAtIndexPaths Update Error

In UICollectionView, I am trying to use performBatchUpdates:completion to perform updates to my grid view. My data source array is self.results.
Here's my code:
dispatch_sync(dispatch_get_main_queue(), ^{
[self.collectionView performBatchUpdates:^{
int resultsSize = [self.results count];
[self.results addObjectsFromArray:newData];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
if (resultsSize == 0) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:0 inSection:0]];
}
else {
for (int i = 0; i < resultsSize; i++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:resultsSize + i inSection:0]];
}
}
for (id obj in self.results)
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
} completion:nil];
Explanation of what I have / what I'm doing:
This code runs fine when the initial insertion into the collection view is done. However, when I add/insert more data into my collection view (by updating self.results and calling this), this gives the following error:
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid
number of items in section 0. The number of items contained in an
existing section after the update (8) must be equal to the number of
items contained in that section before the update (4), plus or minus
the number of items inserted or deleted from that section (32
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'
I understand this means that the data source wasn't updated properly. However, when querying my self.results array, I see the new count of the data. I am doing that on the first line using addObjectsFromArray. I also stored the old results size in resultsSize. I use that variable to add the newly added index paths into arrayWithIndexPaths.
Now when adding/inserting items, I tried the following for-loops:
for (id obj in self.results) which is what I am using now. It works initially but further inserts crashes.
for (UIImage *image in newData) works as well initially but further inserts crash.
From the name of the function, I believe that insertItemsAtIndexPaths will insert all items at those index paths without a loop. However, without a loop, the app crashes when it initially tries to populate the data.
I also tried looping from resultsSize + 1 until the new self.results count (which contains the new data) and that also crashes at the initial update.
Any suggestions about what I'm doing wrong?
Thank you,
I see several things wrong here. First, I'm not sure why you're using dispatch_sync, I don't have much experience with GCD, and I couldn't make it work with that in there (it seemed to hang, and the UI was unresponsive). Perhaps someone else can help with that. Second, in the loop where you add index paths, you're looping over resultsSize, which as I understand it, is the size of the array before the update, which is not what you want -- you want to start the new indexes at resultsSize and loop up to resultsSize + newData.count. Lastly, when you call insertItemsAtIndexPaths, you want to do that once, not in a loop. I tried this, and it worked to update the collection view (I didn't try it from scratch with an empty collection view):
-(void)addNewCells {
[self.collectionView performBatchUpdates:^{
int resultsSize = [self.results count];
[self.results addObjectsFromArray:newData];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
for (int i = resultsSize; i < resultsSize + newData.count; i++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
}
completion:nil];
}

loop Nsmutable arrray of uiimages from end to beginning

I have an NSMUtable array of images where a different image is displayed with a previous and a next button, but when I get to the end of the array the simulator crashes. I want to loop the end of the array to the beginning so that when I get to the end of the array of images when I hit the next button again it loops back to the first image, also when Im at the first image if I hit the previous button it loops to the last image with no crashes
What you want is a circular array, which is easy to implement using a standard NSMutableArray. For instance, say you store your images in an array called imageArray and use a simple variable to keep track of the index of your current image, like:
int currentImageIndex = 0;
...then you might implement nextImage and previousImage like:
- (UIImage*) nextImage {
currentImageIndex = (currentImageIndex + 1) % [imageArray count];
return [imageArray objectAtIndex:currentImageIndex];
}
- (UIImage*) previousImage {
currentImageIndex--;
if (currentImageIndex < 0) {
currentImageIndex = [imageArray count] - 1;
}
return [imageArray objectAtIndex:currentImageIndex];
}
Then just use nextImage and previousImage whenever you want to step through the array, and problem solved.
Simple enough to do. all you need to do is create a check to see if you're on the last element, and if so, set your tracker (like count or i etc) to 0 again,
Heres the psudo code
//set index
if ( array [ index ] == len(array) - 1) //at end
{
index = 0
}
if(array [index] == -1)//at beginning
{
index = len(array) -1
}
// do something with array[index]
I want to propose this solution:
A property for the selected UIImage, a NSInteger property to hold the current index.
-(IBAction) nextButtonTapped:(id)sender
{
self.currentIndex = self.currentIndex++ % [self.images count];
self.selectedImage= [self.images objectAtIndex:self.currentIndex];
[self reloadImageView];
}
-(IBAction) previousButtonTapped:(id)sender
{
self.currentIndex--;
if (self.currentIndex < 0)
self.currentIndex += [self.images count];
self.selectedImage= [self.images objectAtIndex:self.currentIndex];
[self reloadImageView];
}
-(void)reloadImageView
{
//do, what is necessary to display new image. Animation?
}

Label display not instant with iPhone app

I am developing an application for the iPhone. The question I have is how to display a new label with a different text every .5 seconds. For example, it would display Blue, Red, Green, Orange and Purple; one right after one another. Right now I am doing this:
results = aDictionary;
NSArray *myKeys = [results allKeys];
NSArray *sortedKeys = [myKey sortedArrayUsingSelector:#selector(caseInsensitiveCompare:)];
int keyCount = [sortedKeys count];
while (flag == NO) {
NSTimeInterval timeMS = [startDate timeIntervalSinceNow] * -10000.0;
if (timeMS >= i) {
ii++;
i += 1000;
NSLog(#"endDate = %f", timeMS);
int randomNumber = rand() % keyCount + 1;
lblResult.text = [results valueForKey:[sortedKeys objectAtIndex:(randomNumber - 1)]];
result = [results valueForKey:[sortedKeys objectAtIndex:(randomNumber - 1)]];
lblResult.text = result;
}
if (ii > 25) {
flag = YES;
}
}
lblResult.text = [results valueForKey:[sortedKeys objectAtIndex:(sortedKeys.count - 1)]];
this function is called at the viewDidAppear Function and currently isn't displaying the new labels. It only displays the one at the end. Am I doing anything wrong? What would be the best method to approach this?
The problem is that you're not giving the run loop a chance to run (and therefore, drawing to happen). You'll want to use an NSTimer that fires periodically and sets the next text (you could remember in an instance variable where you currently are).
Or use something like this (assuming that items is an NSArray holding your strings):
- (void)updateText:(NSNumber *)num
{
NSUInteger index = [num unsignedInteger];
[label setText:[items objectAtIndex:index]];
index++;
// to loop, add
// if (index == [items count]) { index = 0; }
if (index < [items count]) {
[self performSelector:#selector(updateText:) withObject:[NSNumber numberWithUnsignedInteger:index] afterDelay:0.5];
}
}
At the beginning (e.g. in viewDidAppear:), you could then call
[self updateText:[NSNumber numberWithUnsignedInteger:0]];
to trigger the initial update.
You'd of course need to ensure that the performs are not continuing when your view disappears, you could do this by canceling the performSelector, or if you're using a timer, by simply invalidating it, or using a boolean, or ...
And if you want to get really fancy, use GCD :)