From 7e9dde514baadd712d080f9feaf4cf1e2a246a2d Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Fri, 22 May 2026 08:51:08 +0300 Subject: [PATCH] Fix iOS simulator touch regressions on iOS 26 + ParparVM BGPainter NPE Three independent fixes surfaced while testing on iOS 26.3 simulator under Xcode 26: 1. Component.animate(): replace `getClass() != BGPainter.class` with `!(bgp instanceof BGPainter)`. The class-literal comparison was being bypassed when bgp was a Component$BGPainter constructed via the no-arg / Style / Painter constructors (motion fields null), so the Form-as-animation path ran `bgp.animate()` and tripped a NullPointerException on `wMotion.isFinished()` ~600ms after every form show. instanceof is the intended check anyway -- BGPainter has no subclasses. 2. CN1TapGestureRecognizer: implement `shouldBeRequiredToFailByGestureRecognizer:` to defeat the `_keyboardDismissalGestureRecognized:`-target UITapGestureRecognizer that iOS 26 auto-installs on every UIWindow. Without this, that window-level recognizer (cancelsTouchesInView=YES) consumed tap-down events before our recognizer's touchesBegan: ran, so all taps were dropped. 3. UIHoverGestureRecognizer on the GL view: explicitly clear cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded. The UIGestureRecognizer defaults to cancelsTouchesInView=YES, and on simulator builds the host mac cursor is always hovering over the window, so the hover recognizer was preempting touches. Verified end-to-end on iPhone 17 Pro / iOS 26.3 simulator: form renders, tap fires pointerPressed + Button.actionPerformed, no recurring NPE in the EDT loop. --- .../src/com/codename1/ui/Component.java | 2 +- .../nativeSources/CN1TapGestureRecognizer.m | 22 ++++++++++++++++--- .../CodenameOne_GLViewController.m | 7 ++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/Component.java b/CodenameOne/src/com/codename1/ui/Component.java index 368e0ec7e3..d25adfa0e1 100644 --- a/CodenameOne/src/com/codename1/ui/Component.java +++ b/CodenameOne/src/com/codename1/ui/Component.java @@ -7000,7 +7000,7 @@ public boolean animate() { Painter bgp = getStyle().getBgPainter(); boolean animateBackgroundB = bgp != null && - bgp.getClass() != BGPainter.class && + !(bgp instanceof BGPainter) && bgp instanceof Animation && ((Animation) bgp).animate(); animateBackground = animateBackgroundB || animateBackground; diff --git a/Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m b/Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m index a7ef051572..21edcea83a 100644 --- a/Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m +++ b/Ports/iOSPort/nativeSources/CN1TapGestureRecognizer.m @@ -50,8 +50,6 @@ - (void) install:(CodenameOne_GLViewController*)ctrl { self.delegate = self; [ctrl.view.window addGestureRecognizer:self]; CN1useTapGestureRecognizer = YES; - - } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer @@ -62,6 +60,24 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer return false; } +// iOS 26 auto-installs a UITapGestureRecognizer on every UIWindow with +// _keyboardDismissalGestureRecognized: as its action. Its default +// cancelsTouchesInView=YES, plus its position in the window-level recognizer +// chain ahead of ours, means it can consume tap-down events before CN1TapGR +// ever sees touchesBegan -- the simulator's indirect-pointer-as-touch +// dispatch tickles this path. Insist that CN1TapGR is required to fail +// before any window-level UITapGestureRecognizer can recognise, so we keep +// first crack at every touch regardless of what iOS installed. +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer + shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + if (gestureRecognizer == self + && [otherGestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] + && otherGestureRecognizer.view == self.view) { + return YES; + } + return NO; +} + /** * Some events need to be ignored. We only want to receive events originating from our view hierarchy * that is controlled by the GLViewController. @@ -87,7 +103,7 @@ -(BOOL)ignoreEvent:(UITouch*)touch { } - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event -{ +{ [super touchesBegan:touches withEvent:event]; POOL_BEGIN(); if(touchesArray == nil) { diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index c612a92d49..4942a3cd56 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -2785,6 +2785,13 @@ - (void)cn1InstallHoverRecognizer { UIHoverGestureRecognizer *hover = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(cn1HandleHover:)]; + // UIGestureRecognizer defaults cancelsTouchesInView to YES, which on + // simulator builds where the host-mac mouse cursor is always hovering + // over the window can cancel taps before they reach touchesBegan:. + // Hover is independent of touch; don't let it preempt. + hover.cancelsTouchesInView = NO; + hover.delaysTouchesBegan = NO; + hover.delaysTouchesEnded = NO; [self.view addGestureRecognizer:hover]; #ifndef CN1_USE_ARC [hover release];