UIScrollView remove subview animated - objective-c

What I have:
Basically I have collection of UIViews presented in horizontal scrollView. UIScrollView have paging enabled and each item is not full screen so I can see part of previous and next view based on this.
Each view have delete button on itself.
What I need:
I need to find good idea to animate reloading UIScrollView content after removing one of the subview. As I thought about it there will be 4 different kinds of deletion:
1. deletion of last view
2. deletion of first view
3. deletion of middle view - hardest one
4. deletion of only view - easiest one, just animation of view disappearing
I need some suggestions/solutions.
Thanks,
Michal
edit:
Ok, I havent implemented it fully.
Here is reload method:
- (void)loadScrollView{
for(UIView* view in [_scrollView subviews])
{
[view removeFromSuperview];
}
CGRect frame = self.scrollView.frame;
frame.origin.x = 0;
frame.origin.y = 0;
int i = 0;
_scrollView.clipsToBounds = NO;
_scrollView.pagingEnabled = YES;
_scrollView.showsHorizontalScrollIndicator = NO;
for(MPContact* contact in self.items)
{
frame.origin.x = self.scrollView.frame.size.width*i;
MyView *view = [[[NSBundle mainBundle] loadNibNamed:#"MyView" owner:self options:nil] objectAtIndex:0];
[view setFrame:frame];
i++;
[view.nameAndSurnameLabel setText:#"view text"];
[view setDelegate:self];
[self.scrollView addSubview:view];[self.viewsArray addObject:view];
}[self.scrollView setContentSize:CGSizeMake(frame.size.width*[self.items count], frame.size.height)];}
Each MyView have delete button with delegate, delegate removes view from viewsArray and run this method for now.

I suggest you to use + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion method of UIView class. Just get your UIView that you want to delete, from UIScrollView and set view.alpha = 0 in animations block. In completion block remove this UIView from UIScrollView, using removeFromSuperView method. Setting alpha to 0 will make fade animation.
EDIT:
Iterate all your views you want to move and change it's x coordinate.
for(UIView *view in scrollview.subviews)
{
[UIView animateWithDuration:0.1 animations:^{
[view setFrame:CGRectMake(view.x-(/*HERE YOU NEED TO PUT WIDTH OF TOUR DELETING VIEW*/),view.y,view.width,view.heigh)];
}
completion:^(BOOL finished){
//remove view from superview and change your uiscrollview's content offset.
}];
}

Related

UICollectionView Views Layer Priority

I'm seeing a very odd behavior with UICollectionViews.
Here is the scenario.
I have a UIViewController that has been pushed on to a UINavigationController stack.
The UIViewController view has nav bar and UICollectionView in grid layout. 3 cells wide by unlimited tall.
Just below extent of screen, I also have a UIToolbar hidden. The UIToolbar is on top of UICollectionView in layer hierarchy.
I then allow the user to put view in to "edit mode" and I animate UIToolbar on to the screen and covers bottom portion of UICollectionView. If user leaves "edit mode" I move UIToolbar back off screen.
While in "edit mode" I allow the user to multi select cells with check boxes that appear and uitoolbar has delete button.
Delete does the following:
- (void)deletePhotos:(id)sender {
if ([[self.selectedCells allKeys] count] > 0) {
[[DataManager instance] deletePhotosAtIndexes:[self.selectedCells allKeys]];
[self.selectedCells removeAllObjects];
[self.collectionview reloadData];
[self.collectionview performBatchUpdates:nil completion:nil];
}
}
// Data Manager method in singleton class:
- (void)deletePhotosAtIndexes:(NSArray *)indexes {
NSMutableIndexSet *indexesToDelete = [NSMutableIndexSet indexSet];
for (int i = 0; i < [indexes count]; i++) {
[indexesToDelete addIndex:[[indexes objectAtIndex:i] integerValue]];
NSString *filePath = [self.photosPath stringByAppendingPathComponent:[self.currentPhotos objectAtIndex:[[indexes objectAtIndex:i] integerValue]]];
NSString *thumbnailPath = [self.thumbPath stringByAppendingPathComponent:[self.currentPhotos objectAtIndex:[[indexes objectAtIndex:i] integerValue]]];
if ([[NSFileManager defaultManager] fileExistsAtPath: filePath]) {
[[NSFileManager defaultManager] removeItemAtPath: filePath error:NULL];
[[NSFileManager defaultManager] removeItemAtPath: thumbnailPath error:NULL];
}
}
[self.currentPhotos removeObjectsAtIndexes:indexesToDelete];
}
The data manager contains photo objects and are used in cell creation like so.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
ImageCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"imageCell" forIndexPath:indexPath];
NSString *pngfile = [[[DataManager instance] thumbPath] stringByAppendingPathComponent:[[[DataManager instance] currentPhotos] objectAtIndex:indexPath.row]];
if ([[NSFileManager defaultManager] fileExistsAtPath:pngfile]) {
NSData *imageData = [NSData dataWithContentsOfFile:pngfile];
UIImage *img = [UIImage imageWithData:imageData];
[cell.imageView setImage:img];
}
if ([self.selectedCells objectForKey:[NSString stringWithFormat:#"%d", indexPath.row]] != nil) {
cell.checkbox.hidden = NO;
} else {
cell.checkbox.hidden = YES;
}
return cell;
}
So here is where I'm finding issues:
When deleting enough cells so that number of visible rows changes, UIToolbar is disappearing. In case of a full row of 3, if I only delete 1 or 2 items, the UIToolbar doesn't disappear. I am not doing any animation on the UIToolbar in delete method and only when hitting a Done button that ends edit mode. I've confirmed that this method isn't being called.
I've also confirmed that the UIToolbar isn't actually moving. If I add "self.collectionview removeFromSuperView" on hitting delete in cases where UIToolbar would normally disappear, the UIToolbar is exactly where expected on the screen. This gives me the impression the UICollectionView is somehow changing layer hierarchy in draw of parent view.
I've attempted trying to bringSubviewToFront for UIToolbar and sendSubviewToBack for collectionview and has no affect.
Re-iniating open toolbar causes uitoolbar to animate back in. Oddly, however, it seems to animate from below screen! This makes no sense unless the UICollectionView is somehow pushing the UIToolbar off the screen due after the point where I would be calling the removeFromSuperview call so I can't re-create.
One "solution" I have is to force the UIToolbar to come back in to position but without animation after a 0.01 second delay
[self performSelector:#selector(showToolbarNoAnimation) withObject:nil afterDelay:0.01];
This works.
Here is the question:
Any idea why UICollectionView causing this behavior to push UIToolbar offscreen after a full row is deleted? The hack works but doesn't explain the issue.
Thanks,
James
When you use auto layout, and your views are loaded from a storyboard (or xib), you can't set the frames of your views. Doing so may seem to work initially, but at some point auto layout will reset the view's frame based on the constraints, and you won't understand what happened, and then you'll post a question to stack overflow.
If you need to change the layout of a view, you need to update the view's constraints instead.
There is a constraint specifying the distance between the bottom edge of your toolbar and the bottom edge of its superview. Presumably that distance is -44 (where 44 is the height of the toolbar).
You need to connect that constraint to an outlet in your view controller. The outlet will have type NSLayoutConstraint *. Call it toolbarBottomEdgeConstraint.
When you want to animate the toolbar onto the screen, set constraint's constant to zero and call layoutIfNeeded in an animation block:
- (void)showToolbarAnimated {
[UIView animateWithDuration:0.25 animations:^{
self.toolbarBottomEdgeConstraint.constant = 0;
[self.toolbar layoutIfNeeded];
}];
}
To hide the toolbar, set the constraint's constant back to its original value:
- (void)hideToolbarAnimated {
[UIView animateWithDuration:0.25 animations:^{
self.toolbarBottomEdgeConstraint.constant = -toolbar.bounds.size.height;
[self.toolbar layoutIfNeeded];
}];
}

Pop up tutorial page like Photosynth's

I want to create something that is basically a clone of what photosynth does for their tutorial page. A small "?" button pops up what looks like a new view in a frame that is slightly smaller than the first view, so that you can still see the first view around the edges.
It's a little tough to see from the pic above, but the part around the edges is the old view that the tutorial display popped up over.
My first guess is that I need to use a container view somehow, but I can't find anything on the web about exactly how to do this. I can currently create a container view, hook it up to a new view controller via a segue, and do whatever I want in that new view controller, but the container view is always visible on the view it is contained within. Any help?
BTW, I'm using storyboarding with ARC.
You can add a transparent view to the key window, add a tap gesture recognizer that would dismiss it and the subviews to show the content:
#define OVERLAY_TAG 997
-(void)showTutorial
{
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *overlay = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
overlay.backgroundColor = [UIColor clearColor];
overlay.userInteractionEnabled = YES;
[keyWindow addSubview:overlay];
UITapGestureRecognizer * tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(dismissTutorial)];
CGFloat border = 10;
CGRect frame = overlay.bounds;
// 20 is the status bar height (sorry for using the number)
frame = CGRectMake(border, border + 20, frame.size.width - border * 2, frame.size.height - border * 2 - 20);
// the black view in the example is probably a scroll view
UIView *blackView = [[UIView alloc] initWithFrame:frame];
blackView.backgroundColor = [UIColor blackColor];
blackView.alpha = 0.0;
[overlay addSubview:dimView];
// add all the subviews for your tutorial
// make it appear with an animation
[UIView animateWithDuration:0.3
animations:^{dimView.alpha = 1;}
completion:^(BOOL finished){[overlay addGestureRecognizer:tapRecognizer];}];
}
-(void)dismissTutorial
{
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *overlay = [keyWindow viewWithTag:OVERLAY_TAG];
[UIView animateWithDuration:0.3
animations:^{
overlay.alpha = 0.0;
}
completion:^(BOOL finished){
[overlay removeFromSuperview];
}];
}
This way you would remove the tutorial with a simple tap but you can use a button for instance.

Does presentViewController automatically destroy previous views?

I use a bunch of custom segues to segue from one view controller view to another using:
[self performSegueWithIdentifier:#"createTeamAccountSegue" sender:self];
My question is, do the previous views automatically get destroyed in iOS5 and 6?
I keep having to create custom segues to go to the next view, and then create yet another new segue animation in reverse to go back to the last view. As long as they dont keep stacking up and up then this should be fine right?
EDIT: I should probably show you the general layout of what my custom UIStoryboardSegue class looks like:
- (void) perform {
UIViewController *sourceViewController = (UIViewController *) self.sourceViewController;
UIViewController *destinationViewController = (UIViewController *) self.destinationViewController;
UIView *parent = sourceViewController.view.superview;
[parent addSubview:destinationViewController.view];
[parent sendSubviewToBack:destinationViewController.view];
sourceViewController.view.layer.masksToBounds = NO;
sourceViewController.view.layer.cornerRadius = 8; // if you like rounded corners
sourceViewController.view.layer.shadowOffset = CGSizeMake(0,0);
sourceViewController.view.layer.shadowRadius = 10;
sourceViewController.view.layer.shadowOpacity = 1;
destinationViewController.view.frame = CGRectMake(0, 20, destinationViewController.view.frame.size.width, destinationViewController.view.frame.size.height);
sourceViewController.view.frame = CGRectMake(0, 20, sourceViewController.view.frame.size.width, sourceViewController.view.frame.size.height);
[UIView animateWithDuration:.3 delay:0.0 options:UIViewAnimationCurveEaseInOut animations:^
{
sourceViewController.view.frame = CGRectMake(0, parent.frame.size.height, sourceViewController.view.frame.size.width, sourceViewController.view.frame.size.height);
} completion:^(BOOL finished)
{
[sourceViewController presentViewController:destinationViewController animated:NO completion:NULL];
}];
}
If you are doing there segues in a UINavigationController then no, they aren't destroyed. To go back to one you should use [self.navigationController popViewControllerAnimated:YES];

Subview has wrong orientation

I have a custom segue where I am trying to do the reverse of the standard "Cover Vertical" segue transition. I think this should work:
UIView *srcView = ((UIViewController *)self.sourceViewController).view;
UIView *dstView = ((UIViewController *)self.destinationViewController).view;
[dstView setFrame:CGRectMake(0, 0, srcView.window.bounds.size.width, srcView.window.bounds.size.height)];
[srcView.window insertSubview:dstView belowSubview:srcView];
[UIView animateWithDuration:0.3
animations:^{
srcView.center = CGPointMake(srcView.center.x - srcView.frame.size.width, srcView.center.y);
}
completion:^(BOOL finished){
[srcView removeFromSuperview];
}
];
The problem is the destination view shows up in portrait orientation even though every other view in the app is landscape orientation. Also, the x and y coordinates are reversed. Why is this happening?
I have set up shouldAutorotateToInterfaceOrientation in every view controller, but that didn't help. I could rotate the view with CGAffineTransformMakeRotation, but that doesn't seem like the right thing to do; coordinates would still be reversed, for one thing.
Update:
I tried using CGAffineTransformRotate to rotate the view , but it looks ridiculous. Most of the background shows up black. I don't think this is the way it's suppose to work.
Since I was not able the get the correct orientation and framing of the destination view before the controllers exchange, I did reverse the process:
save the source view as an image (see Remove custom UIStoryboardSegue with custom animation ) ;
switch the current controller [presentViewController: ...];
then, do the animation with the (now correct) destination view embedding the saved source image.
Here is the complete code :
#include <QuartzCore/QuartzCore.h>
#implementation HorizontalSwipingSegue
- (void) perform {
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
UIView *sourceView = source.view;
UIView *destinationView = destination.view;
// Create a UIImageView with the contents of the source
UIGraphicsBeginImageContext(sourceView.bounds.size);
[sourceView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *sourceImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *sourceImageView = [[UIImageView alloc] initWithImage:sourceImage];
[[self sourceViewController] presentViewController:[self destinationViewController] animated:NO completion:NULL];
CGPoint destinationViewFinalCenter = CGPointMake(destinationView.center.x, destinationView.center.y);
CGFloat deltaX = destinationView.frame.size.width;
CGFloat deltaY = destinationView.frame.size.height;
switch (((UIViewController *)self.sourceViewController).interfaceOrientation) {
case UIDeviceOrientationPortrait:
destinationView.center = CGPointMake(destinationView.center.x - deltaX, destinationView.center.y);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaX, sourceImageView.center.y);
break;
case UIDeviceOrientationPortraitUpsideDown:
destinationView.center = CGPointMake(destinationView.center.x + deltaX, destinationView.center.y);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaX, sourceImageView.center.y);
break;
case UIDeviceOrientationLandscapeLeft:
destinationView.center = CGPointMake(destinationView.center.x, destinationView.center.y - deltaY);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaY, sourceImageView.center.y);
break;
case UIDeviceOrientationLandscapeRight:
destinationView.center = CGPointMake(destinationView.center.x, destinationView.center.y + deltaY);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaY, sourceImageView.center.y);
break;
}
[destinationView addSubview:sourceImageView];
[UIView animateWithDuration:0.6f
animations:^{
destinationView.center = destinationViewFinalCenter;
}
completion:^(BOOL finished){
[sourceImageView removeFromSuperview];
}];
}
#end
UPDATED ANSWER:
Make sure you are setting your dstView's frame correctly. I think you'll have better luck using:
dstView.frame = srcView.frame;
instead of:
[dstView setFrame:CGRectMake(0, 0, srcView.window.bounds.size.width, srcView.window.bounds.size.height)];
the bounds property does not react to changes in orientation the same way frame does.
Apparently, it was much easier than I thought to do the segue I wanted. Here's is what worked:
- (void)perform {
UIViewController *src = (UIViewController *)self.sourceViewController;
[src dismissViewControllerAnimated:YES completion:^(void){ [src.view removeFromSuperview]; }];
}
Hopefully this helps someone else.
Replace
[srcView insertSubview:dstView belowSubview:srcView];
with
[srcView insertSubview:dstView atIndex:0];
If you are just adding another view controller's view to your view hierarchy, that view controller is not going to get any information about the orientation of the device, so it will present the view in its default configuration. You need to add the view controller into the view controller hierarchy as well, using the containment methods described here. You also need to remove it from the hierarchy when done.
There are also several good WWDC talks on the subject, one from 2011 called "Implementing view controller containment" and one from this year, the evolution of view controllers.

How to pass scrolling input to a different view

This site really is awesome.
I have what is hopefully a simple question this time. I would like to pass any scrolling input from the user (could be wheel, touchpad, etc) to an NSScrollView which contains my own subviews.
At the moment if the user scrolls just on the documentView (outside of my subviews' frames) the scroll works normally but if they scroll while the cursor is over a subview nothing happens. So basically I'd like to have the subview recognise the scroll event and pass it back to the scroll view.
Any help is greatly appreciated.
Cheers.
EDIT:
Here is the code I'm using to add the subviews to the documentView
_milestoneView and _activityView are both NSView subclasses which have a corresponding nib (created with instantiateNibWithOwner and objects hooked up accordingly) they contain a NSTextField, PXListView and some have a NSProgressIndicator.
-(void)useProject:(NSNumber *)projectId
{
[self resetView];
NSRect bounds = [[self view] bounds];
NSRect defaultFrame = NSMakeRect(20, NSMaxY(bounds)-93, NSMaxX(bounds)-20, 50);
//Prepare the milestone view
if (_milestoneView == nil)
_milestoneView = [MilestoneView milestoneViewFromNibWithFrame:defaultFrame withProject:[BCLocalFetch projectForId:projectId]];
[_milestoneView reloadList];
//Prepare the activity view
if (_activityView == nil)
_activityView = [ActivityView activityViewFromNibWithFrame:defaultFrame withProject:[BCLocalFetch projectForId:projectId]];
[self refresh];
}
I then use the refresh method to reposition them as the content sizes vary so I wanted to have a separate method.
-(void)refresh
{
//Position the milestones first
[_milestoneView setFrameOrigin:NSMakePoint(20, NSMaxY([[self view] bounds])-[_milestoneView frame].size.height-60)];
if ([[_milestoneView milestones] count] > 0)
[[self view] addSubview:_milestoneView];
//Now the activity view
[_activityView setFrameOrigin:NSMakePoint(20, [_milestoneView frame].origin.y-[_activityView frame].size.height-20)];
[[self view] addSubview:_activityView];
[self autosizeView];
}
-(void)autosizeView
{
//Resize the view to accommodate all subviews
NSRect oldFrame = [[self view] frame];
CGFloat lastY = [_activityView frame].origin.y;
if (lastY < 0) {
CGFloat newHeight = oldFrame.size.height + (-lastY);
[[self view] setFrameSize:NSMakeSize(oldFrame.size.width, newHeight)];
}
[[NSNotificationCenter defaultCenter] postNotificationName:#"BBContentDidResizeNotification" object:self];
}
Ok so I came back to the issue and finally got it worked out. The implementation was actually quite simple.
I added a property to PXListView to point to the NSScrollView that is to be scrolled.
I then implemented NSResponder's scrollWheel: method like this:
-(void)scrollWheel:(NSEvent *)theEvent
{
//Pass scrolling to the superview
[_scrollHandler scrollWheel:theEvent];
}
And all is well!