Skip to content

iOS: fix status-bar tap-to-scroll-to-top on iOS 13+ scenes (#3589)#4904

Merged
shai-almog merged 3 commits intomasterfrom
fix-3589-ios-statusbar-tap
May 10, 2026
Merged

iOS: fix status-bar tap-to-scroll-to-top on iOS 13+ scenes (#3589)#4904
shai-almog merged 3 commits intomasterfrom
fix-3589-ios-statusbar-tap

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Fixes #3589. iOS 13+ scene-based apps received UIStatusBarTapAction (visible in log show) but UIKit silently filtered our scrollsToTop proxy out of the dispatch, so the gesture was a no-op on device. Verified the fix on iPhone 15 Pro Max iOS 26 Simulator: tapping the status bar now triggers the listener and scrolls the form to top.

Root cause

Three things had to be true for UIKit to actually call scrollViewShouldScrollToTop: on our proxy and for the synthesized tap to land on CN1's StatusBar:

  • alpha = 0.0 caused iOS 13+ to skip the proxy entirely. Switched to alpha = 1.0 with a fully transparent backgroundColor (still invisible to the user, but iOS counts it as visible).
  • Full-screen frame also got skipped. The proxy is now pinned to just the status-bar strip via the new cn1UpdateStatusBarTapProxyFrame helper (FlexibleWidth + FlexibleBottomMargin so it stays glued to the top on rotation).
  • Synthesized tap at y=0 landed above CN1's StatusBar Container. The form has a small top padding/margin before the toolbar, so y=0 hit Toolbar itself rather than the bar that has the scroll-to-top listener. cn1FireStatusBarTap now aims at safeAreaInsets.top/2 (in native pixels) so the synthesized tap lands inside the StatusBar bar.

Diagnostic counters exposed via Display.getProperty("cn1.iosStatusBarTap.*") are unchanged so users can still verify on a real device.

Changes

  • Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m — proxy alpha, frame, autoresizing, and the synthesized tap y-coordinate.
  • CodenameOne/src/com/codename1/ui/Toolbar.javasetGrabsPointerEvents(true) on the StatusBar Container so Form.getResponderAt(displayWidth/2, …) finds it.
  • Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java — simulator's "iOS Status Bar Tap" menu now locates the StatusBar component and aims the synthesized tap inside it; report text updated to match.

Test plan

  • iPhone 15 Pro Max iOS 26 Simulator — tap status bar → Form.pointerPressed/Released fires at (displayWidth/2, safeAreaInsets.top/2)StatusBar Container's pointer-released listener runs → form scrolls to top.
  • Real iPhone (any iOS 13+) — same flow.
  • JavaSE simulator → Simulate → "iOS Status Bar Tap" — diagnostic now reports the StatusBar component & its bounds, then dispatches the tap which scrolls the form.
  • Forms without paintsTitleBarBool=true — no regression (status bar tap was already a no-op on those).

🤖 Generated with Claude Code

iOS dispatched UIStatusBarTapAction to the scene client but never
called scrollViewShouldScrollToTop: on the proxy scroll view, so the
tap silently no-op'd on device (verified iPhone 15 Pro Max iOS 26).
Three things had to change for UIKit's filter to accept the proxy and
for the synthesized tap to land on CN1's StatusBar bar:

* alpha=1.0 (was 0.0) with a clearColor background -- iOS 13+ filters
  fully-transparent scroll views out of scrollsToTop dispatch.
* frame pinned to the status-bar strip (was full-screen) via the new
  cn1UpdateStatusBarTapProxyFrame helper -- a full-screen frame also
  got skipped.
* synthesize the tap at safeAreaInsets.top/2 in native pixels (was
  y=0) so it lands inside Toolbar's StatusBar Container instead of
  above it (the Form has a small top padding before the toolbar).

Also call setGrabsPointerEvents(true) on the StatusBar Container so
Form.getResponderAt finds it for diagnostics, and update the JavaSE
simulator's "iOS Status Bar Tap" menu to mirror the new tap
coordinates (locate the actual StatusBar component and aim inside).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 9, 2026

Compared 7 screenshots: 7 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 9, 2026

Compared 90 screenshots: 90 matched.

Native Android coverage

  • 📊 Line coverage: 10.22% (5582/54613 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.04% (27399/340806), branch 3.68% (1201/32660), complexity 4.69% (1467/31285), method 8.26% (1207/14620), class 13.68% (269/1966)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 10.22% (5582/54613 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.04% (27399/340806), branch 3.68% (1201/32660), complexity 4.69% (1467/31285), method 8.26% (1207/14620), class 13.68% (269/1966)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 867.000 ms
Base64 CN1 encode 186.000 ms
Base64 encode ratio (CN1/native) 0.215x (78.5% faster)
Base64 native decode 1048.000 ms
Base64 CN1 decode 368.000 ms
Base64 decode ratio (CN1/native) 0.351x (64.9% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 9, 2026

Compared 90 screenshots: 90 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 127 seconds

Build and Run Timing

Metric Duration
Simulator Boot 60000 ms
Simulator Boot (Run) 0 ms
App Install 12000 ms
App Launch 7000 ms
Test Execution 235000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1489.000 ms
Base64 CN1 encode 1481.000 ms
Base64 encode ratio (CN1/native) 0.995x (0.5% faster)
Base64 native decode 1157.000 ms
Base64 CN1 decode 1453.000 ms
Base64 decode ratio (CN1/native) 1.256x (25.6% slower)
Base64 SIMD encode 537.000 ms
Base64 encode ratio (SIMD/native) 0.361x (63.9% faster)
Base64 encode ratio (SIMD/CN1) 0.363x (63.7% faster)
Base64 SIMD decode 483.000 ms
Base64 decode ratio (SIMD/native) 0.417x (58.3% faster)
Base64 decode ratio (SIMD/CN1) 0.332x (66.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 107.000 ms
Image createMask (SIMD on) 17.000 ms
Image createMask ratio (SIMD on/off) 0.159x (84.1% faster)
Image applyMask (SIMD off) 186.000 ms
Image applyMask (SIMD on) 133.000 ms
Image applyMask ratio (SIMD on/off) 0.715x (28.5% faster)
Image modifyAlpha (SIMD off) 162.000 ms
Image modifyAlpha (SIMD on) 64.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.395x (60.5% faster)
Image modifyAlpha removeColor (SIMD off) 219.000 ms
Image modifyAlpha removeColor (SIMD on) 72.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.329x (67.1% faster)
Image PNG encode (SIMD off) 1730.000 ms
Image PNG encode (SIMD on) 1396.000 ms
Image PNG encode ratio (SIMD on/off) 0.807x (19.3% faster)
Image JPEG encode 800.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 9, 2026

Compared 90 screenshots: 90 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 416 seconds

Build and Run Timing

Metric Duration
Simulator Boot 119000 ms
Simulator Boot (Run) 2000 ms
App Install 26000 ms
App Launch 15000 ms
Test Execution 309000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 2336.000 ms
Base64 CN1 encode 2361.000 ms
Base64 encode ratio (CN1/native) 1.011x (1.1% slower)
Base64 native decode 1443.000 ms
Base64 CN1 decode 1593.000 ms
Base64 decode ratio (CN1/native) 1.104x (10.4% slower)
Base64 SIMD encode 591.000 ms
Base64 encode ratio (SIMD/native) 0.253x (74.7% faster)
Base64 encode ratio (SIMD/CN1) 0.250x (75.0% faster)
Base64 SIMD decode 701.000 ms
Base64 decode ratio (SIMD/native) 0.486x (51.4% faster)
Base64 decode ratio (SIMD/CN1) 0.440x (56.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 125.000 ms
Image createMask (SIMD on) 15.000 ms
Image createMask ratio (SIMD on/off) 0.120x (88.0% faster)
Image applyMask (SIMD off) 194.000 ms
Image applyMask (SIMD on) 115.000 ms
Image applyMask ratio (SIMD on/off) 0.593x (40.7% faster)
Image modifyAlpha (SIMD off) 212.000 ms
Image modifyAlpha (SIMD on) 157.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.741x (25.9% faster)
Image modifyAlpha removeColor (SIMD off) 216.000 ms
Image modifyAlpha removeColor (SIMD on) 125.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.579x (42.1% faster)
Image PNG encode (SIMD off) 1686.000 ms
Image PNG encode (SIMD on) 1837.000 ms
Image PNG encode ratio (SIMD on/off) 1.090x (9.0% slower)
Image JPEG encode 1420.000 ms

shai-almog and others added 2 commits May 9, 2026 20:55
The proxy needs alpha=1.0 with a clearColor backgroundColor so iOS 13+
keeps routing UIStatusBarTapAction → scrollViewShouldScrollToTop: to
it, but compositing it through CALayer's renderInContext: (cn1_captureView's
fallback path) still introduced a 1-pixel diff that broke every iOS
screenshot test. Walk the captured hierarchy, set every
CN1StatusBarTapProxyView's alpha to 0 around the render calls, restore
afterwards. The existing alpha<=0 short-circuit in cn1_renderViewIntoContext
takes care of the rest, and iOS doesn't see the temporary alpha change
since the capture runs synchronously on the main thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix hid the proxy via alpha=0 inside cn1_captureView, which
appeared to deadlock the Metal screenshot CI (TransformCamera was the
last test to start before the 30-min timeout). Drop the alpha-toggling
hack and instead install the proxy as a sibling of self.view at the
UIWindow level, not as a descendant.

* iOS still walks the entire window hierarchy when routing
  UIStatusBarTapAction → scrollViewShouldScrollToTop:, so the proxy is
  still found and the status-bar gesture keeps working.
* cn1_captureView calls drawViewHierarchyInRect: / renderInContext: on
  self.view, which only traverses self.view's subtree. With the proxy
  outside that subtree, screenshot tests are byte-identical to before
  the #3589 fix.

cn1InstallStatusBarTapProxy now waits until self.view.window is set --
viewDidLoad runs before the view enters a window, so the actual
installation happens on the viewDidAppear retry path that was already
in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit 0f14499 into master May 10, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tapping a button in the StatusBar does not work on iOS device (iPhone Xr)

1 participant