I'm trying out the animation stuff with the iPhone and I have a small app that has circles falling from the top of the screen to the bottom. I added the UIViewAnimationOptionAllowUserInteraction option to the animation and set userInteractionEnabled = YES on both my sub views and my main view. I am generating the subview programmatically everytime my NSTimer fires, which makes the circles.
The problem I am having is that even if I implement - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event on my main view and my subviews, the event only ever fires on the main view regardless of how hard or how many times I smash the falling circles.
How do I get that event to fire on my circles instead? Why doesn't the touch fire on the view on top?
The problem is that when you perform a "tween" animation with Core Animation (which occurs implicitly when you use the UIView animation methods), the new value is immediately applied to what's known as the model layer tree.
The animation is committed to Core Animation, and the next time the main run loop occurs, Core Animation does the necessary rendering to make it look as if an animation is occurring. In reality, though, the model values of your view (e.g., its frame or transform properties) are changed immediately.
Try tapping at the bottom of the screen where the buttons land (assuming they finish on screen). If you do this, it should respond to the interaction event as you expect.
You could manually change the frame of your circles by constantly firing a timer and applying small changes at each step. This, however, is likely not ideal.
Another option is to make use of your view's layer's presentationLayer method. This layer represents the state of the layer at any point in time, as we see it on the screen. Off the top of my head, I imagine that your main view's touch handling method can iterate through each subview and check to see if the touch point intersects the frame of the subview's layer's presentation layer. If it does, then that means someone "touched" your subview.
for (UIView *subview in self.subviews)
{
CALayer *presentationLayer = (CALayer *)[subview.layer presentationLayer];
if (CGRectContainsPoint(presentationLayer.frame, touchPoint)
{
// Found the subview the user touched
}
}
I should note that presentationLayer returns id, because the presentation layer may be CALayer or any of its subclasses. So, for example, if you're using CAShapeLayer, then -presentationLayer will return a CAShapeLayer. Still, it's fine to just cast it to a CALayer since all you care about for the purposes of this task is to check its frame property.
Related
I have a view which is using single touch events (single finger) for drawing (lines, cycles, text, and so on).
Now I want to put this view inside of UIScrollView, which will allow zooming and panning. Of course two fingers are required to perform both zooming and panning.
What is the pattern do do that? I've found only examples when contents of UIScrollView accepts only single clicks (it contains only a buttons). Nothing what to do when contents require also touch moves.
Access the panGestureRecognizer property on the UIScrollView, and set its minimumNumberOfTouches property to two:
myScrollView.panGestureRecognizer.minimumNumberOfTouchesRequired = 2;
I'm thinking you should begin with scrolling disabled for scrollview and user interaction enabled for both. Then in touchesBegan: you should check for number of touch points. If it's only one (aka user wants to draw) you do nothing (disabling interaction for scrollview would disable it for all subviews as well). However, if the number of touch points is greater than one, enable scrolling then do
[UIView setUserInteractionEnabled: NO]
This way, no lines should be drawn on the UIView when you pinch or zoom with two fingers on the UIScrollView.
I helped developed an app that required a signature plate with in a scroll view, and a scroll view's subviews are weird to touch events, sometimes you have to hold your finger there for it to pass through the scroll view and reach the inner views, so there was a bit of a lag... but what i ended up doing was subclassing a UIScrollView and overriding the
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {}
method and if the touch landed with in the frame of the signature plate it would
[scrollView setScrollEnabled:NO]; and the drawing happened sooner and more smoother... the only problem is they couldn't scroll the scroll view from that box.. basically i created a dead spot with in the scroll view
but, that's a little wonky, i think a UITableView might work better for you... just make it 1 giant table cell, i think you will get better results....
Don't forget to enable 'Multiple Touch' for these features.
I have a UIView which has an X origin that makes it off screen to the right. Then, I do a keyframe animation with CATransform3D:
[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-view.width, 0, 0)]
The problem is, after the animation completes, the view's frame is visually in the correct place, but it still thinks it's off screen, so I can't interact with it. Logging its frame property also shows that it's offscreen, but visually, it's not.
The fill mode for the animation is kCAFillModeForwards, so the final value of the animation sticks.
What is the solution to this problem, that is, interacting with this view after the animation and notifying the view that it is indeed visible?
You can use CAAnimation delegate method -animationDidStop:finished: to fix it when the animation done:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
...
}
The better answer is to use UIView animation methods like animateWithDuration:animations:completion: (and variants on that method.)
Those methods let you do animations on the actual view properties, and when the animation is complete, the view is at it's actual destination location.
The center property is the easiest way to move views around. The frame property isn't valid if you've applied a transform, but the center property IS always valid for both reading and writing.
Core Animation only creates the illusion that your views move. The animation takes place on a display-only copy of the layer tree. You can query the properties of an animating layer by reading those same properties from the layer's presentationLayer. However, the view objects will not respond to the user interaction at their apparent location. The views still think they are at their original locations.
As the other poster said, you could use the animation completion delegate method to move the view to it's final location once the animation is over, but that's a lot of fussy work to do something that UIView animation does for you much more cleanly and simply.
Can you interact with the view even before you animate it?
is the view.userInteractionEnabled is set to YES??
I'm building an iOS board game (similar to scrabble or words) that involves moving around little tiles on the screen and I'm finding the user sometimes has a hard time touching and moving the tiles around because they're too small. Due to the design of the game, I can't increase the size of the tiles, so I've had to implement some little tricks that make touching and moving the tiles easier for the user and they work well. The only issue that remains is the user sometimes touches just barely outside the tile and when the user tries to move it, the tile stays where it is.
I have two ideas for how I can fix this...
If a tile isn't touched when the user touches the screen, I can use the parent view's touch location to find the closest tile to the touch location and somehow forward the touch event to that tile's view.
If a tile isn't touched when the user touches the screen, I can somehow "catch" the tile when the user drags their finger over it as they attempt to move it.
I'd prefer to implement solution #2 since solution #1 has too many problems associated with, not to mention solution #2 is a more realistic experience. That said, how can I "catch" a tile when the user drags their finger over it and send it touch events to move it where the finger is?
Currently, my tiles are implemented as subclasses of the UIView and they handle the touch events (touchesBegain, touchesMoved, and touchesEnded) directly. So if the user touches just barely outside the view and drags their finger over the view, it doesn't receive any of the touch events since it didn't receive the initial touchesBegan event.
Thanks in advance for all your wisdom!
Maybe you should have the "board" view handle all the dragging. When a touch begins and there is a tile at that point, then start dragging it. Otherwise check whenever the touch is moved and as soon as you find a tile, start dragging it.
You could override hitTest:withEvent: in the board view so that it can still detect when a touch hits a tile, but always return itself so that touch events go to the board view (e.g. record the subview that was hit in a separate member variable, so that you know what to start dragging later on when touch events start coming in).
More Details
When handling touches, UIView will use hitTest to find the view that should receive touch events. The default implementation checks each subview so that the "deepest" subview in the hierarchy gets the touches. In order for the board view to receive touches, you would have to disable userInteraction on all of the tile views. But that means you can't use hitTest to find the tile that was touched, since it ignores views that have userInteraction disabled.
So what I am saying is leave userInteraction enabled on the touch views. But override hitTest on the board view so that it first calls the super implementation in order to find a tile (if the result is self, the board itself was hit). No need to implement your own tile searching. Something like this:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if ( hitView != self )
self.draggingTile = hitView;
return self;
}
Now you know what tile to move in touchesMoved. However, I don't think hitTest is called as the touch is moved, so if no tile has been picked up yet, you may have to call it manually (you can get the point and event from the touch passed to touchesMoved.
Have you looked into the UIGestureRecognizer API? I think your best option would be to add a UIPanGestureRecognizer recognizer to your board's view which would then fire back the selector to your UIViewController.
Setup in ViewDidLoad:
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[[self view] addGestureRecognizer:panGestureRecognizer];
And then implemented the selector:
- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer{
CGPoint currentPoint = [gesture locationInView:[self view]];
}
When you set up the gesture recognizer you can set parameters to limit the callbacks (only recognize pans with 1 finger, etc. And in the callback you can check the gesture properties to see if the pan in continuing or if it's coming to an end. You can also grab the current point and determine if it's in or near a tile.
I have an animation I'm using to snap a rotatable UIView to a circular grid. As the view animates, I need to update another view based on the rotating views position.
How can I go about getting the position of the rotating view as it animates into position?
When animation starts you can start a function that will be polling current view position using a timer (I suppose you will need to get presentationLayer from view's layer and get position values from it).
There's no callbacks for CAAnimation other than animationDidStart and animationDidStop so it seems that's the solution (if I'm not mistaken it was also mentioned so in one of WWDC videos).
I'm making an app where one of the objects needs to appear not to move or rotate during AutoRotation, but everything else needs to rotate and be repositioned. The way I did this is by manually rotating the object, and moving it into the same position, relative to the device, that it had prior to rotation. The problem is that it looks funny, appearing to rotate into the same position it had before.
If I suppress the animation effects during rotation, it would appear that the object never moved, and everything else just snapped into place, which is what I want. I haven't been able to find anything in the documentation that tells me how to do this though. How do I do this?
When it comes to UIViewController rotation there are two ways you can set up so you get a chance to rotate and move your own views.
In the one-step process, in your UIViewController-derived class provide an implementation of willAnimateRotationToInterfaceOrientation. It gets called and you can kick off your object rotation so while the system is rotating everything else your object is getting counter-rotated so it looks like it's staying put. You'll want to calculate how much to counter-rotate and in which direction based on current interface orientation vs. new orientation.
The other way is to get notified to do custom rotation in two steps. For example, in the first half you can shrink the object down, move it, and rotate it part way then in the second half finish the rotation as you scale back up to normal size. It's a pretty clever way to make the rotation animation look smoother to the eye.
For the two-step process, you need to define two methods. willAnimateFirstHalfOfRotationToInterfaceOrientation gets called for the first half of the rotation (i.e. up to 45 degrees for a 90 degree rotation and at 90 degrees for an upside down flip). Once past that point the second half is called via willAnimateSecondHalfOfRotationFromInterfaceOrientation.
If your object has a 1:1 aspect ratio (i.e square or round) and in the middle of the view then the one-step process will probably work fine. But if it's a non-square object and has to move position (for example if it's at position 40, 60 in portrait but moves to 20, 100 in landscape) and maybe even needs a bit of scaling to look better then you may want to try the two-step process and see if it looks smoother.
If your object is inside its own individual UIView then it's pretty easy to schedule the rotations through UIView animations. Just create a transform through CGAffineTransformMakeRotation, then inside a pair of UIView beginAnimations/commitAnimations blocks set the transform property of the view to this value. You can tweak the timing through setAnimationDuration.
EDIT: Based on the comments, I'm adding some code to show how you could attach the view to the top-level window instead of to the view controller. Your object would then reside in this view instead of the one managed by controller (which is getting rotated). You still need to over-ride the UIViewController rotate methods, but instead of rotating an object under control of the view controller you would trigger a counter-rotation in the object on the top-level.
To add a view to the top-level window:
YourAppDelegate* windowDelegate = ((YourAppDelegate*) [UIApplication sharedApplication].delegate);
[windowDelegate.window addSubview:yourView];
Keep a reference to yourView somewhere you can get to, then in the UIViewController's willAnimateRotationToInterfaceOrientation counter-rotate yourView, i.e. calculate how much to rotate the view in reverse to where you're going--if the phone is turning 90-degrees clockwise, you'll want to rotate the view back 90 degrees counter-clockwise, etc. Then use UIView animations on yourView.
If you don't want rotation animations, overload -shouldAutorotateToInterfaceOrientation: in your UIViewController and return NO. You can then observe UIDeviceOrientationDidChangeNotification to receive manual orientation change notifications.
This may work:
Listen for willRotateToInterfaceOrientation
[UIView setAnimationsEnabled:NO];
Listen for UIDeviceOrientationDidChangeNotification
[UIView setAnimationsEnabled:YES];
Untested, but I know that does work for some cases.