From 09477a5388022f46c954974e93058ebe1b877df1 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Thu, 12 Nov 2026 12:57:37 +0200 Subject: [PATCH] iOS: keep the port's keyboard handlers out of the SMS composer The keyboard notifications are process wide, so the keyboard of the native SMS composer reached keyboardWillShow/keyboardWillHide as if one of our own text fields had raised it: they recorded a keyboard height, called into Java and repainted underneath the sheet. Issue #6211 reports the composer intermittently opening with its entry field behind the keyboard. sendSMS now suspends that handling while the composer is presented. A show is ignored, and so is the hide that pairs with it, unless one of our own keyboards is still on record -- that hide is what restores the view. The flag clears itself when nothing is presented, so it cannot outlive the sheet and break text editing. Co-Authored-By: Claude Fable 5.2 --- .../CodenameOne_GLViewController.m | 50 +++++++++++++++++++ Ports/iOSPort/nativeSources/IOSNative.m | 10 ++++ 2 files changed, 60 insertions(+) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 48c61cf7326..ea20cd667da 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -4582,6 +4582,40 @@ CGRect setOriginY(CGFloat y, CGRect frame) { int keyboardSlideOffset; int keyboardHeight; +// Set while a system sheet that owns its own keyboard is presented over the +// application -- today the SMS composer (IOSNative sendSMS). The keyboard +// notifications are process wide, so the composer's keyboard reaches the two +// handlers below exactly as one of our own text fields' would: they record a +// keyboard height, call into Java (which can pad and revalidate the current +// form, and fires the virtual keyboard listeners of the application) and +// repaint underneath the sheet. None of that is about our UI, and issue #6211 +// reports the composer intermittently coming up with its entry field behind +// the keyboard, so while the flag is set a "will show" is ignored. +// +// A "will hide" is never ignored on the flag alone. The one that pairs with an +// ignored show is dropped (cn1KeyboardShowIgnored), so the listeners never see +// a "hidden" for a keyboard they were not told about -- but only while we have +// no keyboard of our own on record (keyboardIsShown). When one of OUR editors +// had the keyboard up as the sheet opened, UIKit may hand that keyboard to the +// sheet without a hide in between; the next hide is then the only one our own +// keyboard will ever get, and it is what puts the view and the form back. +BOOL cn1KeyboardTrackingSuspended = NO; +static BOOL cn1KeyboardShowIgnored = NO; + +// A flag that stayed set would silently break every text field in the +// application, so it is not trusted by itself: with nothing presented there is +// no sheet whose keyboard this could be, and the flag is dropped. +static BOOL cn1IsKeyboardTrackingSuspended(UIViewController* controller) { + if (!cn1KeyboardTrackingSuspended) { + return NO; + } + if (controller.presentedViewController == nil) { + cn1KeyboardTrackingSuspended = NO; + return NO; + } + return YES; +} + - (void)keyboardWillHide:(NSNotification *)n { @@ -4589,6 +4623,14 @@ - (void)keyboardWillHide:(NSNotification *)n // renamed one, so this is inert on the native macOS port until it is ported. #if TARGET_OS_OSX #else + if (cn1KeyboardShowIgnored) { + cn1KeyboardShowIgnored = NO; + if (!keyboardIsShown) { + // The keyboard of a system sheet going away, and none of ours to + // put away; see cn1KeyboardTrackingSuspended. + return; + } + } @synchronized([CodenameOne_GLViewController instance]) { [currentTarget removeAllObjects]; } @@ -4653,6 +4695,11 @@ - (void)keyboardWillShow:(NSNotification *)n // renamed one, so this is inert on the native macOS port until it is ported. #if TARGET_OS_OSX #else + if (cn1IsKeyboardTrackingSuspended(self)) { + cn1KeyboardShowIgnored = YES; + return; + } + cn1KeyboardShowIgnored = NO; // Hide the datepicker if it is currently showing. [self datePickerCancel]; @@ -6152,6 +6199,9 @@ -(void) mailComposeController:(MFMailComposeViewController*)controller didFinish // only the body would leave a signature naming an unknown type. #if !TARGET_OS_OSX -(void) messageComposeViewController:(MFMessageComposeViewController*)controller didFinishWithResult:(MessageComposeResult)result { + // Our own keyboard handling resumes here; the hide of the composer's keyboard + // that follows is still dropped, by cn1KeyboardShowIgnored. + cn1KeyboardTrackingSuspended = NO; // Through the composer, for the reason given on the mail delegate above. [controller dismissViewControllerAnimated:YES completion:nil]; } diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 5aec42bdaef..e17c2fac5ce 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -12341,6 +12341,8 @@ void com_codename1_impl_ios_IOSNative_requestAppStoreReview__(CN1_THREAD_STATE_M #endif // CN1_USE_APPREVIEW } +extern BOOL cn1KeyboardTrackingSuspended; + void com_codename1_impl_ios_IOSNative_sendSMS___java_lang_String_java_lang_String(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject, JAVA_OBJECT number, JAVA_OBJECT text) { // Not implemented on the native macOS port: the body below is UIKit -- a @@ -12376,6 +12378,14 @@ void com_codename1_impl_ios_IOSNative_sendSMS___java_lang_String_java_lang_Strin [picker setBody:smsBody]; + // The composer brings its own keyboard; keep our keyboard handlers out + // of it until the delegate dismisses the sheet (issue #6211). Not when + // something is already presented: UIKit then refuses this presentation, + // the delegate that clears the flag never runs, and the keyboard of + // whatever is up would go unhandled until that is dismissed. + if (cn1PresentingController().presentedViewController == nil) { + cn1KeyboardTrackingSuspended = YES; + } [cn1PresentingController() presentModalViewController:picker animated:YES]; #ifndef CN1_USE_ARC