Working on a little iPhone puzzle game using cocos2d-iphone and in this game the user can touch an object and drag it around. The user can also tap in an open space to place an object.
Doing some testing of the touch handling code and first notice a minor item that didn’t match my expectations:
When you drag an object off the physical screen you don’t get
touchesCancelled message, you get
touchesEnded. Unexpected. Seems like a “cancel” to me. But ok, can code around that easily enough.
Then I decided to test what happens when someone calls the phone while the user is dragging an object. This one gets stranger.
When the call comes in the “Answer / Decline” overlay comes up over the game screen and if you leave your finger down on the screen and move it defies expectations (at least mine!):
– first you get a
touchesCancelled call (and no touchesEnded). This is good.
– Then you get a
touchesBegan call (even though you didn’t release and re-touch! Even though your views aren’t even in the front any more!).
– Then you get a
touchesMoved call (or many depending on your finger movement).
– Then, if you release to go press the “Decline” button (this game is good – they can leave a message! 🙂 ha!), you get a
If you instead release and then touch and drag and release again, you’ll get the Touches- Begin/Move/End sequence again even though you can’t see the game board and there is another view in front.
This is highly unexpected to me since I’m used to Cocoa where you touch the view you touch in (or window) and not THROUGH some view onto another (especially when the view you’re touching “through” has buttons and is thus obviously wanting it’s own touches!).
Actually, turns out this isn’t even what the docs suggest should happen. In Event Handling Guide for iOS: Event Types and Delivery say:
Touch events. The window object uses hit-testing and the responder chain to find the view to receive the touch event. In hit-testing, a window calls
hitTest:withEvent:on the top-most view of the view hierarchy; this method proceeds by recursively calling
pointInside:withEvent:on each view in the view hierarchy that returns
YES, proceeding down the hierarchy until it finds the subview within whose bounds the touch took place. That view becomes the hit-test view.
Sure seems like the “Accept/Decline” view which is visually in front should get these events and not the view behind it…
So, how to tell if the touches are for your game board or the “Accept/Decline” overlay?
Turns out there is an
applicationState property of the shared
UIApplication that will indicate that you are in an “inactive” state. So we can use that to ignore the touches when inactive.
Oh. Wait. That’s iOS 4.0 and later only… You want to support 3.1.3 also? Or 3.2 on the iPad? (no calls on that obviously. What else can come up and do something like this? push notification maybe…?). Well, I don’t yet know if a) the problem exists there, or b) how to fix it there if it does!
I’ll have to test that when my iphone running 3.1.3 returns home tonight. Update to follow…
10.10.10 – update:
So, I realized that Timer Done notifications will also create a situation like this. Also SMS message received notifications. (and likely Push Notifications which I didn’t test).
I tried the both the timer (in Clock application) case and the SMS message received case, both in iOS 4.1. In both cases we get the same change in
applicationState instance variable, however the system does not send a
touchesCancelled event and the touch movement keeps going without getting an intermediate
touchesBegan message as the system does for incoming phone call events.
In all three cases the
UIApplicationDelegate protocol method
applicationWillResignActive gets called so that’s definitely a place to consider “fixing” this issue.
Cocos2d-iPhone pauses the animation (well, slows it down dramatically), but does nothing about touch handling.
Haven’t had a chance to try the older iPhone running 3.1.3 yet (busy weekend).
10.10.10 – update 2:
So to work around this I just implemented code to use the
applicationState instance variable on
UIApplication sharedApplication and basically if I get a touch Moved/Ended event and I’m currently tracking a touch then I cancel the tracking and clear the tracking-a-touch flag that I set on touch Begin. If I’m not currently tracking a touch and the application is not active, I just ignore any touch events. Still need to create a 3.1.3 compatible version of this.