From 16edceb56c38b5d1e6287fecc574ba5abcdb65dd Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 3 May 2026 04:47:10 +0300 Subject: [PATCH] iOS: orientation-validate canvas size on iPad foreground (#4767) The previous attempt (a09b9162) silenced viewWillTransitionToSize: during background but then called updateCanvas: synchronously inside cn1ApplicationWillEnterForeground. On iPad with UIScene, view.bounds can still be in the snapshot orientation at that moment, so updateCanvas republished the swapped dimensions through screenSizeChanged -- the same behavior the issue reports as a transient wrong size between stop and start. This change combines two safeguards: it restores the next-runloop deferral around the foreground updateCanvas call so UIKit has a tick to settle the bounds, and it cross-checks the sampled size against the windowScene's interfaceOrientation, swapping the dimensions when they contradict it. The orientation is authoritative for what the user actually sees, so this catches the failure even when the deferred call still fires before UIKit has fully restored bounds. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../nativeSources/CodenameOne_GLAppDelegate.m | 11 +++++- .../CodenameOne_GLViewController.m | 39 ++++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m index 4f71d16772..463241d586 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLAppDelegate.m @@ -248,7 +248,16 @@ - (void)cn1ApplicationWillEnterForeground com_codename1_impl_ios_IOSImplementation_applicationWillEnterForeground__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG); CodenameOne_GLViewController* vc = [CodenameOne_GLViewController instance]; if (vc != nil) { - [vc updateCanvas:YES]; + // Defer to the next runloop so UIKit can settle the view bounds + // after the snapshot rotation. updateCanvas itself also + // orientation-validates the bounds for an extra safety net under + // UIScene on iPad (issue #4767). + dispatch_async(dispatch_get_main_queue(), ^{ + CodenameOne_GLViewController* deferredVc = [CodenameOne_GLViewController instance]; + if (deferredVc != nil) { + [deferredVc updateCanvas:YES]; + } + }); } } diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 6db7066182..6a066edde1 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -167,6 +167,33 @@ static void updateDisplayMetricsFromView(UIView *view) { displayWidth = (int)(view.bounds.size.width * scaleValue); displayHeight = (int)(view.bounds.size.height * scaleValue); } + +// On iPad with UIScene, view.bounds (and even window.bounds) can transiently +// be in the snapshot orientation between sceneDidEnterBackground and the +// first post-foreground layout pass. Cross-check against the windowScene's +// interfaceOrientation -- which reflects what the user actually sees -- and +// swap the dimensions if they contradict it. Without this, sampling bounds +// during the foreground transition publishes a swapped-dimension +// screenSizeChanged event between stop and start (issue #4767). +static CGSize cn1OrientationCorrectSize(UIView *view) { + CGSize size = view.bounds.size; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + UIWindowScene *scene = view.window.windowScene; + if (scene != nil) { + UIInterfaceOrientation o = scene.interfaceOrientation; + if (o != UIInterfaceOrientationUnknown) { + BOOL shouldBePortrait = UIInterfaceOrientationIsPortrait(o); + BOOL isPortrait = size.height >= size.width; + if (shouldBePortrait != isPortrait) { + return CGSizeMake(size.height, size.width); + } + } + } + } +#endif + return size; +} BOOL forceSlideUpField; static UIScrollView *cn1StatusBarTapProxy = nil; @@ -2020,22 +2047,22 @@ -(void)updateCanvas:(BOOL)animated { if(touchesArray != nil) { [touchesArray removeAllObjects]; } - int currentWidth = (int)self.view.bounds.size.width * scaleValue; + CGSize size = cn1OrientationCorrectSize(self.view); //if(currentWidth != displayWidth) { // Note: While it may be tempting to only update the frame buffer if the size has changed, - // doing that causes a bug whereby the app may paint with the wrong dimensions + // doing that causes a bug whereby the app may paint with the wrong dimensions // when opening from the background on iPad with multitasking enabled. // https://github.com/codenameone/CodenameOne/issues/2819 // This may be caused by the fact the getDisplayWidthImpl() and getDisplayHeightImpl() update // the display width/height each time to match the view, without performing other resizing // details, so it is possible that the size change event still needs to be sent // even if the display width already matches the value we're given here. - [[self eaglView] updateFrameBufferSize:(int)self.view.bounds.size.width h:(int)self.view.bounds.size.height]; - updateDisplayMetricsFromView(self.view); - displayWidth = currentWidth; + [[self eaglView] updateFrameBufferSize:(int)size.width h:(int)size.height]; + displayWidth = (int)size.width * scaleValue; + displayHeight = (int)size.height * scaleValue; screenSizeChanged(displayWidth, displayHeight); //} - + } - (BOOL)canBecomeFirstResponder {