UIButton not receiving touch after superview rotate - objective-c

I am working with a UICollectionView, and I have defined a custom UICollectionViewCell by subclassing that class. The initializer of the class defines two views, just like this:
[[NSBundle mainBundle] loadNibNamed:#"ProtelViewCellFlipped" owner:self options:nil];
CATransform3D transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
[flippedView.layer setTransform:transform];
[flippedView setHidden:YES];
[self addSubview:flippedView];
[[NSBundle mainBundle] loadNibNamed:#"ProtelViewCell" owner:self options:nil];
[self addSubview:selfView];
[selfView.layer setZPosition:1];
where flippedView is a view defined by the "ProtelViewCellFlipped" xib file, and selfView is the view defined in the "ProtelViewCell" xib file. The view hierarchy is like this: the view hierarchy http://massimomarra.altervista.org/gerarchia.png
The thing is that the flipped view is rotated (as if it was the other side of the "selfView" view) by a CATransform3D, and then it is hidden. And that's all right, it works.
The selfView view has a UIButton on it. In the bottom right corner of the view. If I press that button, this method gets triggered:
- (void)infoButtonPressed:(UIButton *)sender
{
NSString *animationKey = (selfView.layer.zPosition > flippedView.layer.zPosition)? #"forward" : #"backwards";
CGFloat animationDuration = 0.3;
if (selfView.layer.zPosition > flippedView.layer.zPosition)
[self.layer removeAllAnimations];
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
[rotation setValue:animationKey forKey:#"id"];
rotation.toValue = [NSNumber numberWithFloat:M_PI];
rotation.duration = animationDuration;
rotation.removedOnCompletion = NO;
rotation.fillMode = kCAFillModeForwards;
[self.layer addAnimation:rotation forKey:animationKey];
[self performSelector:#selector(flipLayers) withObject:nil afterDelay:animationDuration/2];
}
So that the self view is rotated with an animation. Also, during the animation gets called this method:
- (void)flipLayers
{
[selfView setHidden:!selfView.hidden];
[flippedView setHidden:!selfView.hidden];
[selfView.layer setZPosition:flippedView.layer.zPosition];
[flippedView.layer setZPosition:(1-selfView.layer.zPosition)];
}
so that the flippedView becomes visible (and selfView hides), and the zPosition of his layer becomes higher than selfView.
So now I have the flippedView visible, and it is displayed correctly. It has also a button, but to trigger his action I have to tap in another position: flipped view scheme: i have to tap where the button "was" before the rotation http://massimomarra.altervista.org/s03KRJAUk7C_nP3yTXb7TBQ.png
It seems like the rotation just modifies the graphics of the view, but not the content.
Can you please help me?
Thanks to you all in advance!!

transform the button back to identity before change orientation
something like this:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
oneButton.transform=CGAffineTransformIdentity;
}
I don't know why but it seems working

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

UIScrollView remove subview animated

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

Replacing a NSSplitView subview with Core Animation

I'm trying to figure out how to animate switching (replacing) subviews in a vertically-configured, 2-view NSSplitView. I've got it semi-working using the following methods in my NSSplitView subclass:
To set up the animation:
- (void)awakeFromNib {
// set delegate
[self setWantsLayer:YES];
CATransition *transition = [CATransition animation];
[transition setType:kCATransitionPush];
[transition setSubtype:kCATransitionFromBottom];
[transition setDuration:1.0];
[self setAnimations:[NSDictionary dictionaryWithObject:transition
forKey:#"subviews"]];
}
And to perform it:
- (void)replaceRightView:(NSView *)newView animated:(BOOL)animate {
NSRect currentSize = [[[self subviews] objectAtIndex:1] frame];
[newView setFrame:currentSize];
if (animate) {
[[self animator] replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
} else {
[self replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
}
}
However, this code has the effect of pushing the entire NSSplitView off, rather than just the subview on the right side of the split.
Is there a way to animate just the subview transition? Perhaps I'm using the wrong animation key ("subviews")? Other animation methods would be fine too.
Thanks!
It's probably not the cleanest way, but I ended up using an NSView 'container' subclass with custom addSubview: and replaceSubview:withView: methods that modify the new subview's frame to match the container view's frame, which is then structured into the NSSplitView subview I wanted to animate. I then setup the CATransition on the container view, and everything worked as I wanted.

detect long press on UINavigationItem's back button

I want to add functionality to my back buttons through my UINavigationController-based app where long-pressing the back button will pop to root. However, I can't figure out where to attach the gesture recognizer. Do I subclass UINavigationBar and try and detect if the long press is in the left button region?
I've heard of people adding similar functionality before. Anyone have any ideas?
I know this question is old, but I came up with a solution. Instead of trying to add the gesture recognizer to the button itself (which would be ideal), I added it to the self.navigationController.navigationBar and then in the action method, use the locationInView to see if I'm over the back button. I wasn't entirely sure about how to identify the back button precisely, so I'm clumsily just grabbing the the first subview with an x coordinate less than some arbitrary value, but it seems promising. If someone has a better way to identify the frame of the back button, let me know.
- (void)longPress:(UILongPressGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
// set a default rectangle in case we don't find the back button for some reason
CGRect rect = CGRectMake(0, 0, 100, 40);
// iterate through the subviews looking for something that looks like it might be the right location to be the back button
for (UIView *subview in self.navigationController.navigationBar.subviews)
{
if (subview.frame.origin.x < 30)
{
rect = subview.frame;
break;
}
}
// ok, let's get the point of the long press
CGPoint longPressPoint = [sender locationInView:self.navigationController.navigationBar];
// if the long press point in the rectangle then do whatever
if (CGRectContainsPoint(rect, longPressPoint))
[self doWhatever];
}
}
- (void)addLongPressGesture
{
if (NSClassFromString(#"UILongPressGestureRecognizer"))
{
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPress:)];
[self.navigationController.navigationBar addGestureRecognizer:longPress];
[longPress release];
}
}
I believe UIGestureRecognizers can only be added to UIViews and subclasses of UIViews.
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html
The back button is a UIBarButtonItem that descends from NSObject. Therefore, you won't be able to attach a gesture recognizer to a standard back button using
UILongPressGestureRecognizer *longPressGesture =
[[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(longPress:)] autorelease];
[self.navigationItem.backBarButtonItem addGestureRecognizer:longPressGesture];
You can however add a custom view to a UIBarButtonItem. A custom view could just as easily be a UIView, UIButton, UILabel, etc.
Example:
UIView *myTransparentGestureView = [[UIView alloc] initWithFrame:CGRectMake(0,0,40,30)];
[myTransparentGestureView addGestureRecognizer:longPressGesture];
[self.navigationItem.backBarButtonItem setCustomView:myTransparentGestureView];
// Or you could set it like this
// self.navigationItem.backBarButtonItem.customView = myTransparentGestureView;
[myTransparentGestureView release];
You have to be careful however, since setting properties on backBarButtonItem applies to the next view that you push. So if you have view A that pushes to view B and you want the gesture to be recognized when you tap back in view B. You must set it up in view A.
I followed a slightly different path, figured I'd share it. The above answers are fine, but really, if the long press is in the leading 1/3 of the nav bar, that's good enough for me:
- (void)longPress:(UILongPressGestureRecognizer *)gr
{
NSLog(#"longPress:");
UINavigationBar *navBar = [self navigationBar];
CGFloat height = navBar.bounds.size.height;
CGPoint pt = [gr locationOfTouch:0 inView:navBar];
//NSLog(#"PT=%# height=%f", NSStringFromCGPoint(pt), height);
if(CGRectContainsPoint(CGRectMake(0,0,100,height), pt)) {
[self popToViewController:self.viewControllers[0] animated:YES];
}
}
Here's my solution:
In appDelegate (the "owner" of the nav bar in my app), In applicationDidFinishLaunchingWithOptions:
Get the nav bar view and add the gesture recognizer to the whole view:
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:#selector(backButtonLongPress:)];
[myNavBar addGestureRecognizer:longPress];
NSLog(#"Gesture Recognizer Added.");
Then in appDelegate, in -(void) backButtonLongPress:(id) sender
Check to see if the gesture occurs within the frame of the back button:
if ([sender state] == UIGestureRecognizerStateBegan) {
// Get the nav bar view
UINavigationBar *myNavBar = nil;
for (UIView *view in [self.window.rootViewController.view subviews]) {
if ([view isKindOfClass:[UINavigationBar class]]) {
NSLog(#"Found Nav Bar!!!");
myNavBar = (UINavigationBar *)view;
}
}
// Get the back button view
UIView *backButtonView = nil;
for (UIView *view in [myNavBar subviews]) {
if ([[[view class] description] isEqualToString:#"UINavigationItemButtonView"]) {
backButtonView = view;
NSLog(#"Found It: %#", backButtonView);
NSLog(#"Back Button View Frame: %f, %f; %f, %f", backButtonView.frame.origin.x, backButtonView.frame.origin.y, backButtonView.frame.size.width, backButtonView.frame.size.height);
}
}
CGPoint longPressPoint = [sender locationInView:myNavBar];
NSLog(#"Touch is in back button: %#", CGRectContainsPoint(backButtonView.frame, longPressPoint) ? #"YES" : #"NO");
if (CGRectContainsPoint(backButtonView.frame, longPressPoint)) {
// Place your action here
}
// Do nothing if outside the back button frame
}

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!