I have an NSCollectionView which is bound to an NSArrayController via Interface Builder.
I provide NSSortDescriptors and NSPredicates to filter and sort the array (which totally works), however, since I am relying on the implicit animation via NSAnimationContext the only key my CALayers are requested to animate is #"hidden". I would like to animate the frame origin so the cells visually move to their new positions.
In the past I've used the collection views animator property to performBatchUpdate's and animate insertions, deletions and moves which resulted in #"frameOrigin" animations to be triggered. It doesnt seem to work that way using bindings?
Is it possible when specifying a sort or filter on the ArrayController which is backing my NSCollectionView via binding, to force explicit frame animation on its items?
Here is my current sorting code, for reference:
- (void) setupSortUsingSortDescriptor:(NSSortDescriptor*) sortDescriptor selectedItem:(SynopsisMetadataItem*)item
{
NSAnimationContext.currentContext.allowsImplicitAnimation = YES;
NSAnimationContext.currentContext.duration = 0.5;
[NSAnimationContext beginGrouping];
self.resultsArrayControler.sortDescriptors = #[sortDescriptor];
[self updateStatusLabel];
if(item != nil)
{
NSUInteger index = [self.resultsArrayControler.arrangedObjects indexOfObject:item];
if(index != NSNotFound)
{
NSIndexPath* newItem = [NSIndexPath indexPathForItem:index inSection:0];
NSSet* newItemSet = [NSSet setWithCollectionViewIndexPath:newItem];
[self.resultsArrayControler setSelectionIndex:index];
[self.collectionView.animator scrollToItemsAtIndexPaths:newItemSet scrollPosition:NSCollectionViewScrollPositionCenteredVertically];
}
}
[NSAnimationContext endGrouping];
}
Thanks, any insight is appreciated!
It is really hard to help without seeing all the code and settings for Collection View. It could have different styles and layout. Collection View with "content array" layout animates items out of the box as soon as you change sortDescriptors of bound NSArrayController.
I created an example for you in Swift (cannot do objective-c sorry):
https://github.com/emankovski/AnmatedCollectionSort
Related
I am trying to create a NSCollectionView programmatically using a NSCollectionViewDataSource.
The code is very simple:
self.collectionView = [[NSCollectionView alloc] init];
// Add collection view to self.view etc.
self.collectionView.dataSource = self;
[self.collectionView registerClass:[NSCollectionViewItem class] forItemWithIdentifier:#"test"]
self.collectionView.collectionViewLayout = gridLayout;
[self.collectionView reloadData]
This leads to the following methods getting called (if I don't set the collectionViewLayout property explicitly these two don't get called either):
- (NSInteger)numberOfSectionsInCollectionView:(NSCollectionView*)collectionView
- (NSInteger)collectionView:(NSCollectionView*)collectionView numberOfItemsInSection:(NSInteger)section
However, collectionView:itemForRepresentedObjectAtIndexPath: is never called. Is there something else that I need to do in order to make sure that the last data source method is called? I have made sure that the two count calls return > 0, so that's not the problem.
So it seems that the problem was actually that I wasn't wrapping the NSCollectionView in a NSScrollView. This probably has to do with the layout being done incorrectly (so the items aren't requested from the data source) if it is not wrapped in a scroll view.
I've been working through different scenario's in the past days, and I dare say that using an NSScrollView, or not, makes practically no difference. With or without scrollView, I've ended up with the same errors and limitations.
What does make a huge difference is the choice between "old school" and the new-fangled collectionView. By "old school" I mean setting the itemPrototype and contents properties, something like this:
NSCollectionView *collectionView = [[NSCollectionView alloc] init];
collectionView.itemPrototype = [TBCollectionViewItem new];
collectionView.content = self.collectionItems;
NSInteger index = 0;
for (NSString *title in _collectionItems) {
NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:0];
TBCollectionViewItem *item = [collectionView makeItemWithIdentifier:#"Test" forIndexPath:path];
item.representedObject = title;
index++;
}
// Plays well with constraints
New school, something along these lines:
NSCollectionView *collectionView = [[NSCollectionView alloc] init];
collectionView.identifier = TBCollectionViewIdentifier;
[collectionView registerClass:[TBCollectionViewItem class] forItemWithIdentifier:TBCollectionViewItemIdentifier]; //register before makeItemWithIdentifier:forIndexPath: is called.
TBCollectionViewGridLayout *gridLayout = [TBCollectionViewGridLayout collectionViewGridLayout:NSMakeSize(250, 100)]; //getting the contentSize from the scrollView does not help
collectionView.collectionViewLayout = gridLayout;
collectionView.dataSource = self;
Now, you may have noticed the comment that registerClass: must be called before makeItemWithIdentifier:forIndexPath. In practice, that means calling registerClass: before setting .dataSource, whereas in your code you set .dataSource first. The docs state:
Although you can register new items at any time, you must not call the makeItemWithIdentifier:forIndexPath: method until after you register the corresponding item.
I wish I could say that by switching those two lines, all layout problems will be solved. Unfortunately, I've found that the .collectionViewLayout / .dataSource combination is a recipe for (auto)layout disaster. Whether that can be fixed by switching from NSCollectionViewGridLayout to flowLayout, I'm not yet certain.
I've been trying to implement an overlaying activity indicator for a UITableView by this tutorial - http://www.markbetz.net/2010/09/30/ios-diary-showing-an-activity-spinner-over-a-uitableview/
It might be a little old but it seems to work well apart from a little issue with the bounds to display the overlay in.
I try to get those bounds here:
-(void)showActivityView {
if (overlayController == nil) {
// This is where I get the wrong bounds
overlayController = [[ActivityOverlayController alloc] initWithFrame:self.tableView.bounds];
}
[self.tableView insertSubview:overlayController.view aboveSubview:self.tableView];
}
And this gets the bounds and displays my overlay perfectly if I call the method AFTER the table is loaded and filled but if I call it before it gets wrong bounds. I've tried getting the bounds of the tableView.superView but this just displays the overlay in the top left corner.
I understand this is because the UITableView doesn't contain any cells before loading and so doesn't have proper bounds yet but I don't know of a way to get these.
Wrong display:
Correct (but after loading table) display:
How about using the UITableView's "frame" property, rather than "bounds"? If the Table View is defined in a XIB, the frame should already be fine before loading cells into the table. So you would need to replace:
overlayController = [[ActivityOverlayController alloc] initWithFrame:self.tableView.bounds];
with:
overlayController = [[ActivityOverlayController alloc] initWithFrame:self.tableView.frame];
According to the example link you should do
-(void)showActivityView {
if (overlayController == nil) {
// This is where I get the wrong bounds
overlayController = [[ActivityOverlayController alloc] initWithFrame:self.tableView.superview.bounds];
}
[self.tableView.superview insertSubview:overlayController.view aboveSubview:self.tableView];
}
Why are you doing [self.tableView insertSubview:..] instead of [self.tableView.superview insertSubview:..]?
Let's imagine that I have 2 UIScrollViews with different UIImageViews inside. When I trigger an action I would like the contents, parameters etc (besides the location) of the 2nd UIScrollView to be passed onto the 1st UIScrollView.
So, here's the code that I've come up with:
-(void) someAction {
UIScrollView * scroll = [[UIScrollView alloc] init]; // create an intermediary scrollView
scroll = secondScroll; // here I intent to pass the content of secondScroll to scroll
scroll.frame = firstScroll.frame; // here I assign the frame of firstScroll to scroll
so that it doesn't take the frame of secondScroll;
firstScroll =scroll; // and then pass all the contents of scroll to firstScroll;
}
But when I execute that and trigger the action, the firstScroll seems to take the content of secondScroll, but secondScroll seems to be deleted. I need it to stay as is.
Any help?
Thanks
-(void) someAction {
UIScrollView * scroll = [[UIScrollView alloc] init]; // create an intermediary scrollView
That's WRONG. You'll be leaking memory with it, since you're losing the pointer to that scroll view after assigning it to a different object. You'd better simply declare UIScrollView *scroll; for that purpose.
scroll = secondScroll; // here I intent to pass the content of secondScroll to scroll
But that's not what happens. The scroll views don't magically copy themselves; if you assign one to another, you only assign the pointer.
scroll.frame = firstScroll.frame; // here I assign the frame of firstScroll to scroll
// so that it doesn't take the frame of secondScroll;
So, from now on, if you operate on scroll, it will effect secondScroll.
firstScroll = scroll; // and then pass all the contents of scroll to firstScroll;
And with this, you assign firstScroll to SecondScroll. So firstScroll will now essentially be secondScroll, losing it (leaking memory again and again) and having done nothing useful.
}
What on Earth are you trying to do with this?
Assuming you want to exchange the two views, you should do something like this instead:
BOOL firstScrollVisible = NO; // in -init or whatever
- (void) someAction
{
if (fistScrollVisible)
[self.view bringSubviewToFront:secondScroll];
else
[self.view bringSubviewToFront:firstScroll];
firstScrollVisible = !firstScrollVisible;
}
Try doing the following:
- (void)someAction {
[firstScroll removeFromSuperview]; // So that we do not leave the old firstScroll as a subview
firstScroll = [secondScroll copy]; // Makes a copy of secondScroll and stores it in firstScroll
[self addSubview:firstScroll]; // Add the new firstScroll to the view
}
Important Note(s):
This code will leak memory, as -copy returns an object with a reference count of +1, but we do not release firstScroll. I highly suggest you make firstScroll and secondScroll into nonatomic, retain properties (#property (nonatomic, retain) UIScrollView *firstScroll). If you want more info on this, just ask. Furthermore, you will need to make sure that firstScroll and secondScroll are never garbage values when this code gets run (it will crash) - depending on when and how it is called you may be fine.
If you want to clone a UI element, you use NSCoder (just like the nib file loader does).
NSData *scrollViewData = [NSKeyedArchiver archivedDataWithRootObject:self.firstScroll];
CGRect oldFrame = self.secondScroll.frame;
self.secondScroll = [NSKeyedUnarchiver unarchiveObjectWithData:scrollViewData];
self.secondScroll.frame = oldFrame;
This serializes the UIView and all its subviews into an NSData and then creates a complete copy of those from the NSData.
#jrtc27 Thanks for your answer. I think I've found a solution and it seems to accomplish what I'm looking for. But I'm not sure if it's the right way or not. Here's what I do:
- (void)someAction {
[[firstScroll subviews] makeObjectsPerformSelector:#selector(removeFromSuperView:);//here I sort of blank out the content of the firstScroll
NSArray * array = [secondScroll subviews];//place all subview of the secondScroll in array
int i = 0;
int nrOfSubviews = [array count];
for (i=0;i<nrOfSubviews;i++) { //appoint them one by one to firstScroll
[firstScroll addSubview:[array objectAtIndex:i]];
}
}
I'm writing a dynamic wizard application using cocoa/objective c on osx 10.6. The application sequences through a series of views gathering user input along the way. Each view that is displayed is provided by a loadable bundle. When the app starts up, a set of bundles are loaded and as the controller sequences through them, it asks each bundle for its view to display. I use the following to animate the transition between views
[[myContentView animator] replaceSubview:[oldView retain] with:newView];
This works fine most of the time. Every once in a while, a view is displayed and some of the subviews are not displayed. It may be a static text field, a checkbox, or even the entire set of subviews. If, for example, a checkbox is not displayed, I can still click where it should be and it then gets displayed.
I thought it might have something to do with the animation so I tried it like this
[myContentView replaceSubview:[oldView retain] with:newView];
with the same result. Any ideas on what's going on here? Thanks for any assistance.
I don't think is good to use this [oldView retain]. This retain do not make sense. The function replaceSubview will retain it if it is necessary.
Because it work "most of the time" I think it is a memory problem. You try to use a released thing. Test without it and see what happens.
I got the same problem, replaceSubview didn't work.
Finally i found something wrong with my code. so here are the rules :
Both subviews should be in MyContentview's subviews array.
OldView should be the topmost subview on MyContentView or replacesubview will not take place.
here is a simple function to perform replacesubview
- (void) replaceSubView:(NSView *)parentView:(NSView *)oldView:(NSView *)newView {
//Make sure every input pointer is not nill and oldView != newView
if (parentView && oldView && newView && oldView!=newView) {
//if newview is not present in parentview's subview array
//then add it to parentview's subview
if ([[parentView subviews] indexOfObject:newView] == NSNotFound) {
[parentView addSubview:newView];
}
//Note : Sometimes you should make sure that the oldview is the top
//subview in parentview. If not then the view won't change.
//you should change oldview with the top most view on parentview
//replace sub view here
[parentView replaceSubview:oldView with:newView];
}
}
So, I iterate through a loop and create UIViews which contain UIImageViews (So that I can selectively show any given part). These UIViews are all stored in a UIScrollView.
I add gesture recognizers to the UIViews in the loop where I created them.
When I run the program, only the items initially visible within the UIScrollView have their gestures recognized. If I scroll over to previously hidden items and then tap on them, nothing happens at all (the gesture is never recognized or attempted to be).
Initialization code:
UITapGestureRecognizer* gestRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
gestRec.delegate = self;
[imageholder addGestureRecognizer:gestRec];
Code that deals with the gesture:
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
float count = [self._imageHolders count];
NSLog(#"handling gesture: %f",count);
while(count--){
UIView* object = (UIView*) [self._imageHolders objectAtIndex:count];
// NSLog(#"Whats going on: %#, %#, %b",object,gestureRecognizer.view, object == gestureRecognizer.view);
if(object == gestureRecognizer.view){
object.alpha = .1;
count = 0;
}
// [object release];
}
}
Any ideas?
---- Update :
I've explored a variety of the available functions in scrollview, UIView and the gesture recognizer and have tried messing with the bounds in case something was cut off that way... Interestingly enough, if there is one item only partially visible and you move it over so it's completely visible, only the portion originally visible will recognize any gestures.
I don't know enough about how the gesture recognizer works within the UIKit architecture to understand this problem. The Apple example for a scrollview with gestures doesn't seem to have this problem, but I can't find any real differences, except that I am nesting my UIImageViews within their own UIViews
I had a similar problem and found it was caused by adding the sub-views to a top-level view then adding that top-level view to the scroll-view. The top-level view had to be sized to the same dimensions as the contentSize (not the bounds) of the scroll-view otherwise it wouldn't pass on touch events to its subviews even when they had scrolled into view.
Try to set the cancelsTouchesInView property to NO.
UITapGestureRecognizer* gestRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
gestRec.delegate = self;
**gestRec.cancelsTouchesInView = NO;**
[imageholder addGestureRecognizer:gestRec];