From f76f4dc0f840b32cc7a5f68f399127ec5860f274 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 13:55:38 -0700 Subject: [PATCH 01/21] fix: [SDK-5654] render IAMs on iPhone Duo folded and unfolded Size the IAM window and WebView from the window scene bounds instead of UIScreen bounds, which report a single panel on foldables. Compute banner heights from the view's safe area insets so relayouts don't collapse them. Co-Authored-By: Claude Opus 5.5 --- .../Controller/OSMessagingController.m | 2 +- .../OneSignalInAppMessages/UI/OSInAppMessageView.m | 5 +++-- .../UI/OSInAppMessageViewController.m | 9 ++++++--- .../OneSignalInAppMessages/UIApplication+OneSignal.h | 1 + .../OneSignalInAppMessages/UIApplication+OneSignal.m | 10 ++++++++++ 5 files changed, 21 insertions(+), 6 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index 704316abb9..52039366a5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -1382,7 +1382,7 @@ - (void)webViewContentFinishedLoading:(OSInAppMessageInternal *)message { if (!self.window) { self.window = [[UIWindow alloc] init]; self.window.windowLevel = UIWindowLevelAlert; - self.window.frame = [[UIScreen mainScreen] bounds]; + self.window.frame = [UIApplication displayBounds]; } self.window.rootViewController = _viewController; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m index aef2b358bf..e1dfc095b2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m @@ -30,6 +30,7 @@ #import "OSInAppMessageClickResult.h" #import #import "OSMacros.h" +#import "UIApplication+OneSignal.h" @interface OSInAppMessageView () @@ -101,7 +102,7 @@ - (void)setupWebviewWithMessageHandler:(id)handler { // WebView should use mainBounds as frame since we need to make sure it spans full possible screen size // to prevent text wrapping while obtaining true height of message from JS - CGRect mainBounds = UIScreen.mainScreen.bounds; + CGRect mainBounds = [UIApplication displayBounds]; mainBounds.size.width -= (2.0 * marginSpacing); // Setup WebView, delegates, and disable scrolling inside of the WebView @@ -132,7 +133,7 @@ - (void)setIsFullscreen:(BOOL)isFullscreen { } - (void)setWebviewFrame { - CGRect mainBounds = UIScreen.mainScreen.bounds; + CGRect mainBounds = [UIApplication displayBounds]; if (!self.isFullscreen) { CGFloat marginSpacing = [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN]; mainBounds.size.width -= (2.0 * marginSpacing); diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 873e7447d6..30cfa9c36f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -32,6 +32,7 @@ #import #import "OSMacros.h" #import "OSSessionManager.h" +#import "UIApplication+OneSignal.h" #define HIGHEST_CONSTRAINT_PRIORITY 999.0f #define HIGH_CONSTRAINT_PRIORITY 990.0f @@ -375,7 +376,7 @@ - (void)addConstraintsForMessage { height = safeArea.heightAnchor; } - CGRect mainBounds = [OneSignalCoreHelper getScreenBounds]; + CGRect mainBounds = [UIApplication displayBounds]; CGFloat marginSpacing = [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN]; let screenHeight = [NSString stringWithFormat:@"Screen Bounds Height: %f", mainBounds.size.height]; @@ -413,9 +414,11 @@ - (void)addConstraintsForMessage { double bannerWidth = mainBounds.size.width; double bannerHeight = self.message.height.doubleValue + (2.0f * marginSpacing); double bannerMessageY = mainBounds.size.height - bannerHeight; + // Use the view's insets, which the safe area constraints use. The window's insets shrink once + // it is banner-sized, so relayouts (e.g. after a size transition) would collapse the message. switch (self.message.position) { case OSInAppMessageDisplayPositionTop: { - UIEdgeInsets safeAreaInsets = self.view.window.safeAreaInsets; + UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; bannerHeight += safeAreaInsets.top + safeAreaInsets.bottom; double statusBarHeight = UIApplication.sharedApplication.statusBarFrame.size.height; bannerHeight += statusBarHeight; @@ -429,7 +432,7 @@ - (void)addConstraintsForMessage { break; } case OSInAppMessageDisplayPositionBottom: { - UIEdgeInsets safeAreaInsets = self.view.window.safeAreaInsets; + UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; bannerHeight += safeAreaInsets.top + safeAreaInsets.bottom; bannerMessageY = mainBounds.size.height - bannerHeight; self.view.window.frame = CGRectMake(0, bannerMessageY, bannerWidth, bannerHeight); diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.h index b1ff274ea0..590a6b1f0a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.h @@ -28,4 +28,5 @@ #import @interface UIApplication (OneSignal) + (BOOL)applicationIsActive; ++ (CGRect)displayBounds; @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m index 9ffec9c074..6f7a1e9787 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m @@ -39,4 +39,14 @@ + (BOOL)applicationIsActive { return [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive; } +// UIScreen bounds can differ from the window scene, e.g. on foldables where the screen +// reports a single panel, and UIKit forces windows to their scene's size. ++ (CGRect)displayBounds { + UIWindowScene *scene = UIApplication.sharedApplication.keyWindow.windowScene; + if (scene) { + return scene.coordinateSpace.bounds; + } + return UIScreen.mainScreen.bounds; +} + @end From 94b049f8e0e466bc64716e73238e33ad940f719e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 14:34:50 -0700 Subject: [PATCH 02/21] chore: [SDK-5654] fill demo top inset with the nav bar color Co-Authored-By: Claude Opus 5.5 --- examples/demo/App/Views/Components/LogView.swift | 2 +- examples/demo/App/Views/ContentView.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/demo/App/Views/Components/LogView.swift b/examples/demo/App/Views/Components/LogView.swift index 96ddb32b38..57d83e5aae 100644 --- a/examples/demo/App/Views/Components/LogView.swift +++ b/examples/demo/App/Views/Components/LogView.swift @@ -42,7 +42,7 @@ struct LogView: View { entryList } .frame(maxWidth: .infinity) - .background(OS.Color.logBackground) + .background(OS.Color.logBackground, ignoresSafeAreaEdges: .horizontal) .accessibilityElement(children: .contain) .accessibilityIdentifier("log_view") } diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index 4c632c9300..610c5f3ab2 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -79,7 +79,8 @@ struct ContentView: View { // Android's `scrollIntoView` `scrollableElement` param. .accessibilityIdentifier("main_scroll_view") } - .background(OS.Color.lightBackground.ignoresSafeArea()) + .background(OS.Color.lightBackground.ignoresSafeArea(edges: [.horizontal, .bottom])) + .background(OS.Color.primary.ignoresSafeArea()) .navigationBarTitleDisplayMode(.inline) .toolbarBackground(OS.Color.primary, for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) From 4577de9f1ad7b356c62e98f6be4eb66fb3704501 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 15:57:22 -0700 Subject: [PATCH 03/21] fix(iam): keep IAM clear of safe area hardware --- .../UI/OSInAppMessageViewController.m | 75 ++++++++++++++----- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 30cfa9c36f..4022272a8e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -54,6 +54,10 @@ @interface OSInAppMessageViewController () // This constraint enforces an aspect ratio for the given message type (ie. banner) @property (strong, nonatomic, nonnull) NSLayoutConstraint *heightConstraint; +// These pin the message view to the left & right, updated as the safe area changes +@property (strong, nonatomic, nullable) NSLayoutConstraint *leftConstraint; +@property (strong, nonatomic, nullable) NSLayoutConstraint *rightConstraint; + // This recognizer lets the user pan (swipe) the message up and down @property (weak, nonatomic, nullable) UIPanGestureRecognizer *panGestureRecognizer; @@ -343,6 +347,45 @@ - (void)encounteredErrorLoadingMessageContent:(NSError * _Nullable)error { [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:message]; } +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + [self updateHorizontalInsets]; +} + +// Each side uses the larger of the margin and its safe area inset, and on iOS 27.1+ keeps the margin +// from hardware inside a side safe area (e.g. a foldable's camera), matching system notification banners. +// Regions flush with the edge, like a status bar column, are already covered by the safe area. +- (void)updateHorizontalInsets { + if (!self.leftConstraint || !self.rightConstraint) { + return; + } + CGFloat margin = self.useWidthMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; + UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : self.view.safeAreaInsets; + CGFloat left = MAX(margin, safeArea.left); + CGFloat right = MAX(margin, safeArea.right); +#if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1 + if (@available(iOS 27.1, *)) { + CGFloat width = self.view.bounds.size.width; + for (UIViewReservedRegion *region in [self.view reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { + CGFloat minX = CGRectGetMinX(region.frame); + CGFloat maxX = CGRectGetMaxX(region.frame); + if (minX >= 1 && maxX <= safeArea.left + 1) { + left = MAX(left, maxX + margin); + } else if (maxX <= width - 1 && minX >= width - safeArea.right - 1) { + right = MAX(right, width - minX + margin); + } + } + } +#endif + // Only assign changes, since this runs on every layout pass + if (self.leftConstraint.constant != left) { + self.leftConstraint.constant = left; + } + if (self.rightConstraint.constant != right) { + self.rightConstraint.constant = right; + } +} + /* Sets up the message view in its initial (hidden) position Adds constraints so that the message view has the correct size @@ -355,13 +398,16 @@ - (void)addConstraintsForMessage { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"addConstraintsForMessage: messageView is not a subview of OSInAppMessageViewController"]; [self.view addSubview:self.messageView]; } - + + // Size transitions call this again, possibly before the appearance animation lowers the initial + // Y constraint, so remove the previous constraints or a stale one keeps the message offscreen. + let previousConstraints = [self.view.constraints filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"firstItem == %@ OR secondItem == %@", self.messageView, self.messageView]]; + [NSLayoutConstraint deactivateConstraints:previousConstraints]; + self.heightConstraint.active = false; + // Initialize the anchors that describe the edges of the view, such as the top, bottom, etc. NSLayoutAnchor *top = self.view.topAnchor, - *bottom = self.view.bottomAnchor, - *leading = self.view.leadingAnchor, - *trailing = self.view.trailingAnchor, - *center = self.view.centerXAnchor; + *bottom = self.view.bottomAnchor; NSLayoutDimension *height = self.view.heightAnchor; // The safe area represents the anchors that are not obscurable by UI such @@ -370,9 +416,6 @@ - (void)addConstraintsForMessage { let safeArea = self.view.safeAreaLayoutGuide; top = safeArea.topAnchor; bottom = safeArea.bottomAnchor; - leading = safeArea.leadingAnchor; - trailing = safeArea.trailingAnchor; - center = safeArea.centerXAnchor; height = safeArea.heightAnchor; } @@ -401,11 +444,9 @@ - (void)addConstraintsForMessage { [self.messageView.heightAnchor constraintLessThanOrEqualToAnchor:height multiplier:1.0 constant:(self.useHeightMargin ? (2*-marginSpacing) : 0)].active = true; // Pins the message view to the left & right - let leftConstraint = [self.messageView.leadingAnchor constraintEqualToAnchor:leading constant:(self.useWidthMargin ? marginSpacing : 0)]; - let rightConstraint = [self.messageView.trailingAnchor constraintEqualToAnchor:trailing constant:(self.useWidthMargin ? -marginSpacing : 0)]; - - // Ensure the message view is always centered horizontally - [self.messageView.centerXAnchor constraintEqualToAnchor:center].active = true; + self.leftConstraint = [self.messageView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor]; + self.rightConstraint = [self.view.rightAnchor constraintEqualToAnchor:self.messageView.rightAnchor]; + [self updateHorizontalInsets]; // Set Y constraints // Since we animate the appearance of the message (ie. slide in from top), @@ -469,8 +510,8 @@ - (void)addConstraintsForMessage { self.initialYConstraint.priority = HIGH_CONSTRAINT_PRIORITY; self.finalYConstraint.priority = MEDIUM_CONSTRAINT_PRIORITY; self.panVerticalConstraint.priority = HIGHEST_CONSTRAINT_PRIORITY; - leftConstraint.priority = MEDIUM_CONSTRAINT_PRIORITY; - rightConstraint.priority = MEDIUM_CONSTRAINT_PRIORITY; + self.leftConstraint.priority = MEDIUM_CONSTRAINT_PRIORITY; + self.rightConstraint.priority = MEDIUM_CONSTRAINT_PRIORITY; // Constraints should all be active except for the panVerticalConstraint, which // is only active when the user is panning (swiping) @@ -478,8 +519,8 @@ - (void)addConstraintsForMessage { self.heightConstraint.active = true; self.initialYConstraint.active = true; self.finalYConstraint.active = true; - leftConstraint.active = true; - rightConstraint.active = true; + self.leftConstraint.active = true; + self.rightConstraint.active = true; // Adding all of these constraints has caused the view's needsLayout property // to become true, so when we call layoutIfNeeded, it causes the view From 3766b065442a549bcb7f1e70075494c1f82bb9a5 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 16:07:58 -0700 Subject: [PATCH 04/21] revert me later --- examples/demo/App/App.swift | 13 ++++++++++--- examples/demo/App/Views/ContentView.swift | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/examples/demo/App/App.swift b/examples/demo/App/App.swift index 7e6d6fd2ce..2b0f874961 100644 --- a/examples/demo/App/App.swift +++ b/examples/demo/App/App.swift @@ -36,9 +36,16 @@ struct App: SwiftUI.App { var body: some Scene { WindowGroup { - ContentView() - .environmentObject(viewModel) - .environmentObject(toastPresenter) + TabView { + ContentView() + .environmentObject(viewModel) + .environmentObject(toastPresenter) + .tabItem { Label("Home", systemImage: "photo.on.rectangle") } + Color.clear + .tabItem { Label("Library", systemImage: "square.stack.fill") } + Color.clear + .tabItem { Label("Search", systemImage: "magnifyingglass") } + } } } } diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index 610c5f3ab2..88939cd5f2 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -128,6 +128,14 @@ struct ContentView: View { } .accessibilityIdentifier("brand_title") } + ToolbarItemGroup(placement: .navigationBarTrailing) { + Button {} label: { Image(systemName: "chevron.left") } + Button {} label: { Image(systemName: "ellipsis") } + Button {} label: { Image(systemName: "square.and.arrow.up") } + Button {} label: { Image(systemName: "heart") } + Button {} label: { Image(systemName: "bookmark") } + Button {} label: { Image(systemName: "trash") } + } } } From c98f33d00c5f7ca3e8fc756634ca1031f976838a Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 16:13:20 -0700 Subject: [PATCH 05/21] fix(iam): query full-screen window for occlusion regions --- .../UI/OSInAppMessageViewController.m | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 4022272a8e..8b510bf578 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -365,10 +365,20 @@ - (void)updateHorizontalInsets { CGFloat right = MAX(margin, safeArea.right); #if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1 if (@available(iOS 27.1, *)) { + // Regions are only reported within a view's bounds, so query a full screen app window + // since banner windows may not cover the hardware. + UIView *regionView = self.view; + for (UIWindow *window in self.view.window.windowScene.windows) { + if (window != self.view.window && CGRectEqualToRect(window.frame, window.windowScene.coordinateSpace.bounds)) { + regionView = window; + break; + } + } CGFloat width = self.view.bounds.size.width; - for (UIViewReservedRegion *region in [self.view reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { - CGFloat minX = CGRectGetMinX(region.frame); - CGFloat maxX = CGRectGetMaxX(region.frame); + for (UIViewReservedRegion *region in [regionView reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { + CGRect frame = [regionView convertRect:region.frame toView:self.view]; + CGFloat minX = CGRectGetMinX(frame); + CGFloat maxX = CGRectGetMaxX(frame); if (minX >= 1 && maxX <= safeArea.left + 1) { left = MAX(left, maxX + margin); } else if (maxX <= width - 1 && minX >= width - safeArea.right - 1) { From 3a331d6e73e8168d634257d6f294980707dab46a Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 16:22:49 -0700 Subject: [PATCH 06/21] refactor(iam): extract sceneView helper method --- .../UI/OSInAppMessageViewController.m | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 8b510bf578..bddff90bf1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -352,6 +352,17 @@ - (void)viewDidLayoutSubviews { [self updateHorizontalInsets]; } +// The app's full screen window. Its safe area and reserved regions cover the whole scene and stay +// stable during size transitions, unlike the banner sized window this message is shown in. +- (UIView *)sceneView { + for (UIWindow *window in self.view.window.windowScene.windows) { + if (window != self.view.window && CGRectEqualToRect(window.frame, window.windowScene.coordinateSpace.bounds)) { + return window; + } + } + return self.view; +} + // Each side uses the larger of the margin and its safe area inset, and on iOS 27.1+ keeps the margin // from hardware inside a side safe area (e.g. a foldable's camera), matching system notification banners. // Regions flush with the edge, like a status bar column, are already covered by the safe area. @@ -365,15 +376,8 @@ - (void)updateHorizontalInsets { CGFloat right = MAX(margin, safeArea.right); #if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1 if (@available(iOS 27.1, *)) { - // Regions are only reported within a view's bounds, so query a full screen app window - // since banner windows may not cover the hardware. - UIView *regionView = self.view; - for (UIWindow *window in self.view.window.windowScene.windows) { - if (window != self.view.window && CGRectEqualToRect(window.frame, window.windowScene.coordinateSpace.bounds)) { - regionView = window; - break; - } - } + // Regions are only reported within a view's bounds, which banner windows may not cover + UIView *regionView = [self sceneView]; CGFloat width = self.view.bounds.size.width; for (UIViewReservedRegion *region in [regionView reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { CGRect frame = [regionView convertRect:region.frame toView:self.view]; @@ -465,11 +469,11 @@ - (void)addConstraintsForMessage { double bannerWidth = mainBounds.size.width; double bannerHeight = self.message.height.doubleValue + (2.0f * marginSpacing); double bannerMessageY = mainBounds.size.height - bannerHeight; - // Use the view's insets, which the safe area constraints use. The window's insets shrink once - // it is banner-sized, so relayouts (e.g. after a size transition) would collapse the message. + // Size banner windows from the scene's insets. This window's own insets shrink once it is banner + // sized and change mid transition, so relayouts would collapse or clip the message. switch (self.message.position) { case OSInAppMessageDisplayPositionTop: { - UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; + UIEdgeInsets safeAreaInsets = [self sceneView].safeAreaInsets; bannerHeight += safeAreaInsets.top + safeAreaInsets.bottom; double statusBarHeight = UIApplication.sharedApplication.statusBarFrame.size.height; bannerHeight += statusBarHeight; @@ -483,7 +487,7 @@ - (void)addConstraintsForMessage { break; } case OSInAppMessageDisplayPositionBottom: { - UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; + UIEdgeInsets safeAreaInsets = [self sceneView].safeAreaInsets; bannerHeight += safeAreaInsets.top + safeAreaInsets.bottom; bannerMessageY = mainBounds.size.height - bannerHeight; self.view.window.frame = CGRectMake(0, bannerMessageY, bannerWidth, bannerHeight); From 262f9211a805cc7688799bbcacf36b83241e4f83 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 16:45:59 -0700 Subject: [PATCH 07/21] fix(iam): use max of margin and bottom safe area --- .../UI/OSInAppMessageViewController.m | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index bddff90bf1..a8bbf28cae 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -350,6 +350,15 @@ - (void)encounteredErrorLoadingMessageContent:(NSError * _Nullable)error { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [self updateHorizontalInsets]; + if (self.message.position == OSInAppMessageDisplayPositionBottom && self.finalYConstraint.constant != -[self bottomInset]) { + self.finalYConstraint.constant = -[self bottomInset]; + } +} + +// Like the sides, bottom banners use the larger of the margin and the bottom safe area rather than their sum +- (CGFloat)bottomInset { + CGFloat margin = self.useHeightMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; + return self.isFullscreen ? margin : MAX(margin, self.view.safeAreaInsets.bottom); } // The app's full screen window. Its safe area and reserved regions cover the whole scene and stay @@ -493,10 +502,8 @@ - (void)addConstraintsForMessage { self.view.window.frame = CGRectMake(0, bannerMessageY, bannerWidth, bannerHeight); self.initialYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:8.0f]; - self.finalYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:bottom - constant:(self.useHeightMargin ? -marginSpacing : 0)]; - self.panVerticalConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:bottom - constant:(self.useHeightMargin ? -marginSpacing : 0)]; + self.finalYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-[self bottomInset]]; + self.panVerticalConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-[self bottomInset]]; break; } case OSInAppMessageDisplayPositionFullScreen: From 402c59c86c8259fd7e648cc14cd57be127b2c37e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 17:12:02 -0700 Subject: [PATCH 08/21] fix(iam): apply safe area offset to top banners --- .../UI/OSInAppMessageViewController.m | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index a8bbf28cae..4a94a18f21 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -350,15 +350,20 @@ - (void)encounteredErrorLoadingMessageContent:(NSError * _Nullable)error { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [self updateHorizontalInsets]; - if (self.message.position == OSInAppMessageDisplayPositionBottom && self.finalYConstraint.constant != -[self bottomInset]) { - self.finalYConstraint.constant = -[self bottomInset]; + if ([self.message isBanner] && self.finalYConstraint.constant != [self bannerYOffset]) { + self.finalYConstraint.constant = [self bannerYOffset]; } } -// Like the sides, bottom banners use the larger of the margin and the bottom safe area rather than their sum -- (CGFloat)bottomInset { +// Like the sides, banners use the larger of the margin and the safe area at their edge rather than their sum. +// Offset of the message's top (top banners) or bottom (bottom banners) from the matching view edge. +- (CGFloat)bannerYOffset { CGFloat margin = self.useHeightMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; - return self.isFullscreen ? margin : MAX(margin, self.view.safeAreaInsets.bottom); + UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : self.view.safeAreaInsets; + if (self.message.position == OSInAppMessageDisplayPositionTop) { + return MAX(margin, safeArea.top); + } + return -MAX(margin, safeArea.bottom); } // The app's full screen window. Its safe area and reserved regions cover the whole scene and stay @@ -428,19 +433,9 @@ - (void)addConstraintsForMessage { [NSLayoutConstraint deactivateConstraints:previousConstraints]; self.heightConstraint.active = false; - // Initialize the anchors that describe the edges of the view, such as the top, bottom, etc. - NSLayoutAnchor *top = self.view.topAnchor, - *bottom = self.view.bottomAnchor; - NSLayoutDimension *height = self.view.heightAnchor; - // The safe area represents the anchors that are not obscurable by UI such // as a notch or a rounded corner on newer iOS devices like iPhone X - if (!self.isFullscreen) { - let safeArea = self.view.safeAreaLayoutGuide; - top = safeArea.topAnchor; - bottom = safeArea.bottomAnchor; - height = safeArea.heightAnchor; - } + NSLayoutDimension *height = self.isFullscreen ? self.view.heightAnchor : self.view.safeAreaLayoutGuide.heightAnchor; CGRect mainBounds = [UIApplication displayBounds]; CGFloat marginSpacing = [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN]; @@ -489,10 +484,8 @@ - (void)addConstraintsForMessage { self.view.window.frame = CGRectMake(0, 0, bannerWidth, bannerHeight); self.initialYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.topAnchor constant:-8.0f]; - self.finalYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:top - constant:(self.useHeightMargin ? marginSpacing : 0)]; - self.panVerticalConstraint = [self.messageView.topAnchor constraintEqualToAnchor:top - constant:(self.useHeightMargin ? marginSpacing : 0)]; + self.finalYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:[self bannerYOffset]]; + self.panVerticalConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:[self bannerYOffset]]; break; } case OSInAppMessageDisplayPositionBottom: { @@ -502,8 +495,8 @@ - (void)addConstraintsForMessage { self.view.window.frame = CGRectMake(0, bannerMessageY, bannerWidth, bannerHeight); self.initialYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:8.0f]; - self.finalYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-[self bottomInset]]; - self.panVerticalConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-[self bottomInset]]; + self.finalYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:[self bannerYOffset]]; + self.panVerticalConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:[self bannerYOffset]]; break; } case OSInAppMessageDisplayPositionFullScreen: From 220f9b6fbc95ad4197a4fb9b4b80bce7c376bcbc Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 17:42:17 -0700 Subject: [PATCH 09/21] refactor(iam): unify vertical insets via layout guide --- .../UI/OSInAppMessageViewController.m | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 4a94a18f21..5eaacfbbbe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -58,6 +58,11 @@ @interface OSInAppMessageViewController () @property (strong, nonatomic, nullable) NSLayoutConstraint *leftConstraint; @property (strong, nonatomic, nullable) NSLayoutConstraint *rightConstraint; +// Area the message is laid out in vertically, inset by the larger of the margin and the safe area +@property (strong, nonatomic, nullable) UILayoutGuide *verticalGuide; +@property (strong, nonatomic, nullable) NSLayoutConstraint *verticalGuideTopConstraint; +@property (strong, nonatomic, nullable) NSLayoutConstraint *verticalGuideBottomConstraint; + // This recognizer lets the user pan (swipe) the message up and down @property (weak, nonatomic, nullable) UIPanGestureRecognizer *panGestureRecognizer; @@ -350,20 +355,18 @@ - (void)encounteredErrorLoadingMessageContent:(NSError * _Nullable)error { - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; [self updateHorizontalInsets]; - if ([self.message isBanner] && self.finalYConstraint.constant != [self bannerYOffset]) { - self.finalYConstraint.constant = [self bannerYOffset]; - } + [self updateVerticalInsets]; } -// Like the sides, banners use the larger of the margin and the safe area at their edge rather than their sum. -// Offset of the message's top (top banners) or bottom (bottom banners) from the matching view edge. -- (CGFloat)bannerYOffset { +// Like the sides, the top and bottom use the larger of the margin and the safe area rather than their sum +- (void)updateVerticalInsets { + if (!self.verticalGuideTopConstraint || !self.verticalGuideBottomConstraint) return; CGFloat margin = self.useHeightMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : self.view.safeAreaInsets; - if (self.message.position == OSInAppMessageDisplayPositionTop) { - return MAX(margin, safeArea.top); - } - return -MAX(margin, safeArea.bottom); + CGFloat top = MAX(margin, safeArea.top); + CGFloat bottom = MAX(margin, safeArea.bottom); + if (self.verticalGuideTopConstraint.constant != top) self.verticalGuideTopConstraint.constant = top; + if (self.verticalGuideBottomConstraint.constant != bottom) self.verticalGuideBottomConstraint.constant = bottom; } // The app's full screen window. Its safe area and reserved regions cover the whole scene and stay @@ -433,9 +436,20 @@ - (void)addConstraintsForMessage { [NSLayoutConstraint deactivateConstraints:previousConstraints]; self.heightConstraint.active = false; - // The safe area represents the anchors that are not obscurable by UI such - // as a notch or a rounded corner on newer iOS devices like iPhone X - NSLayoutDimension *height = self.isFullscreen ? self.view.heightAnchor : self.view.safeAreaLayoutGuide.heightAnchor; + if (!self.verticalGuide) { + self.verticalGuide = [UILayoutGuide new]; + [self.view addLayoutGuide:self.verticalGuide]; + self.verticalGuideTopConstraint = [self.verticalGuide.topAnchor constraintEqualToAnchor:self.view.topAnchor]; + self.verticalGuideBottomConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.verticalGuide.bottomAnchor]; + [NSLayoutConstraint activateConstraints:@[ + self.verticalGuideTopConstraint, + self.verticalGuideBottomConstraint, + [self.verticalGuide.leftAnchor constraintEqualToAnchor:self.view.leftAnchor], + [self.verticalGuide.rightAnchor constraintEqualToAnchor:self.view.rightAnchor] + ]]; + } + [self updateVerticalInsets]; + NSLayoutDimension *height = self.verticalGuide.heightAnchor; CGRect mainBounds = [UIApplication displayBounds]; CGFloat marginSpacing = [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN]; @@ -452,14 +466,14 @@ - (void)addConstraintsForMessage { // Height constraint based on the IAM being full screen or any others with a specific height // NOTE: full screen IAM payload has no height, so match screen height minus margins if (self.message.position == OSInAppMessageDisplayPositionFullScreen) - self.heightConstraint = [self.messageView.heightAnchor constraintEqualToAnchor:height multiplier:1.0 constant:(self.useHeightMargin ? (2*-marginSpacing) : 0)]; + self.heightConstraint = [self.messageView.heightAnchor constraintEqualToAnchor:height]; else self.heightConstraint = [self.messageView.heightAnchor constraintEqualToConstant:self.message.height.doubleValue]; // The aspect ratio for each type (ie. Banner) determines the height normally // However the actual height of the HTML content takes priority. // Makes sure our webview is never taller than our screen. - [self.messageView.heightAnchor constraintLessThanOrEqualToAnchor:height multiplier:1.0 constant:(self.useHeightMargin ? (2*-marginSpacing) : 0)].active = true; + [self.messageView.heightAnchor constraintLessThanOrEqualToAnchor:height].active = true; // Pins the message view to the left & right self.leftConstraint = [self.messageView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor]; @@ -484,8 +498,8 @@ - (void)addConstraintsForMessage { self.view.window.frame = CGRectMake(0, 0, bannerWidth, bannerHeight); self.initialYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.topAnchor constant:-8.0f]; - self.finalYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:[self bannerYOffset]]; - self.panVerticalConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:[self bannerYOffset]]; + self.finalYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.verticalGuide.topAnchor]; + self.panVerticalConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.verticalGuide.topAnchor]; break; } case OSInAppMessageDisplayPositionBottom: { @@ -495,19 +509,14 @@ - (void)addConstraintsForMessage { self.view.window.frame = CGRectMake(0, bannerMessageY, bannerWidth, bannerHeight); self.initialYConstraint = [self.messageView.topAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:8.0f]; - self.finalYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:[self bannerYOffset]]; - self.panVerticalConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:[self bannerYOffset]]; + self.finalYConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.verticalGuide.bottomAnchor]; + self.panVerticalConstraint = [self.messageView.bottomAnchor constraintEqualToAnchor:self.verticalGuide.bottomAnchor]; break; } case OSInAppMessageDisplayPositionFullScreen: case OSInAppMessageDisplayPositionCenterModal: self.view.window.frame = mainBounds; - NSLayoutAnchor *centerYanchor = self.view.centerYAnchor; - if (!self.isFullscreen) { - let safeArea = self.view.safeAreaLayoutGuide; - centerYanchor = safeArea.centerYAnchor; - } - + NSLayoutAnchor *centerYanchor = self.verticalGuide.centerYAnchor; self.initialYConstraint = [self.messageView.centerYAnchor constraintEqualToAnchor:centerYanchor constant:0.0f]; self.finalYConstraint = [self.messageView.centerYAnchor constraintEqualToAnchor:centerYanchor constant:0.0f]; self.panVerticalConstraint = [self.messageView.centerYAnchor constraintEqualToAnchor:centerYanchor constant:0.0f]; From f4e829ac21c66077c4f7c54b25deb48d94da2b88 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Fri, 30 Oct 2026 18:31:26 -0700 Subject: [PATCH 10/21] refactor(iam): compute webview insets before window exists --- .../UI/OSInAppMessageView.h | 2 + .../UI/OSInAppMessageView.m | 5 +-- .../UI/OSInAppMessageViewController.m | 43 +++++++++++-------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h index e8d3aa0b71..b21e221f7b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h @@ -41,6 +41,8 @@ NS_ASSUME_NONNULL_BEGIN @interface OSInAppMessageView : UIView @property (weak, nonatomic, nullable) id delegate; +// Left & right space around the message, so content is laid out at the width it is displayed at +@property (nonatomic) UIEdgeInsets horizontalInsets; - (instancetype _Nonnull)initWithMessage:(OSInAppMessageInternal *)inAppMessage withScriptMessageHandler:(id)messageHandler; - (void)resetWebViewToMaxBoundsAndResizeHeight:(void (^) (NSNumber *newHeight)) completion; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m index e1dfc095b2..263fa67204 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m @@ -134,10 +134,7 @@ - (void)setIsFullscreen:(BOOL)isFullscreen { - (void)setWebviewFrame { CGRect mainBounds = [UIApplication displayBounds]; - if (!self.isFullscreen) { - CGFloat marginSpacing = [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN]; - mainBounds.size.width -= (2.0 * marginSpacing); - } + mainBounds.size.width -= self.horizontalInsets.left + self.horizontalInsets.right; [self.webView setFrame:mainBounds]; } diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 5eaacfbbbe..ac14c0166f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -298,6 +298,7 @@ - (void)parseContentData:(NSDictionary *)data { if (self.isFullscreen) { self.pendingHTMLContent = [self setContentInsetsInHTML:self.pendingHTMLContent]; } + self.messageView.horizontalInsets = [self horizontalInsets]; [self.messageView setIsFullscreen:self.isFullscreen]; } @@ -372,8 +373,10 @@ - (void)updateVerticalInsets { // The app's full screen window. Its safe area and reserved regions cover the whole scene and stay // stable during size transitions, unlike the banner sized window this message is shown in. - (UIView *)sceneView { - for (UIWindow *window in self.view.window.windowScene.windows) { - if (window != self.view.window && CGRectEqualToRect(window.frame, window.windowScene.coordinateSpace.bounds)) { + // Content loads before this message has a window + UIWindowScene *scene = self.view.window.windowScene ?: UIApplication.sharedApplication.keyWindow.windowScene; + for (UIWindow *window in scene.windows) { + if (window != self.view.window && CGRectEqualToRect(window.frame, scene.coordinateSpace.bounds)) { return window; } } @@ -383,23 +386,18 @@ - (UIView *)sceneView { // Each side uses the larger of the margin and its safe area inset, and on iOS 27.1+ keeps the margin // from hardware inside a side safe area (e.g. a foldable's camera), matching system notification banners. // Regions flush with the edge, like a status bar column, are already covered by the safe area. -- (void)updateHorizontalInsets { - if (!self.leftConstraint || !self.rightConstraint) { - return; - } +- (UIEdgeInsets)horizontalInsets { + UIView *sceneView = [self sceneView]; CGFloat margin = self.useWidthMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; - UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : self.view.safeAreaInsets; + UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : sceneView.safeAreaInsets; CGFloat left = MAX(margin, safeArea.left); CGFloat right = MAX(margin, safeArea.right); #if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1 if (@available(iOS 27.1, *)) { - // Regions are only reported within a view's bounds, which banner windows may not cover - UIView *regionView = [self sceneView]; - CGFloat width = self.view.bounds.size.width; - for (UIViewReservedRegion *region in [regionView reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { - CGRect frame = [regionView convertRect:region.frame toView:self.view]; - CGFloat minX = CGRectGetMinX(frame); - CGFloat maxX = CGRectGetMaxX(frame); + CGFloat width = sceneView.bounds.size.width; + for (UIViewReservedRegion *region in [sceneView reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { + CGFloat minX = CGRectGetMinX(region.frame); + CGFloat maxX = CGRectGetMaxX(region.frame); if (minX >= 1 && maxX <= safeArea.left + 1) { left = MAX(left, maxX + margin); } else if (maxX <= width - 1 && minX >= width - safeArea.right - 1) { @@ -408,12 +406,20 @@ - (void)updateHorizontalInsets { } } #endif + return UIEdgeInsetsMake(0, left, 0, right); +} + +- (void)updateHorizontalInsets { + if (!self.leftConstraint || !self.rightConstraint) { + return; + } + UIEdgeInsets insets = [self horizontalInsets]; // Only assign changes, since this runs on every layout pass - if (self.leftConstraint.constant != left) { - self.leftConstraint.constant = left; + if (self.leftConstraint.constant != insets.left) { + self.leftConstraint.constant = insets.left; } - if (self.rightConstraint.constant != right) { - self.rightConstraint.constant = right; + if (self.rightConstraint.constant != insets.right) { + self.rightConstraint.constant = insets.right; } } @@ -937,6 +943,7 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id Date: Mon, 2 Nov 2026 16:39:03 -0800 Subject: [PATCH 11/21] fix(iam): refresh status bar visibility once IAM appears Unfolded (resizable) scenes don't re-query prefersStatusBarHidden when the IAM window becomes key, so full-bleed IAMs kept the status bar over the close button until a rotation. Co-Authored-By: Claude Opus 5.5 --- .../UI/OSInAppMessageViewController.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index ac14c0166f..296b007094 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -146,6 +146,12 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; } +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + // Resizable scenes (e.g. an unfolded display) don't re-query prefersStatusBarHidden when this window becomes key + [self setNeedsStatusBarAppearanceUpdate]; +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; From 07301d479101faab5ec3d88508cc48f924e20918 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 2 Nov 2026 16:39:03 -0800 Subject: [PATCH 12/21] Revert "revert me later" This reverts commit 3766b065442a549bcb7f1e70075494c1f82bb9a5. --- examples/demo/App/App.swift | 13 +++---------- examples/demo/App/Views/ContentView.swift | 8 -------- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/examples/demo/App/App.swift b/examples/demo/App/App.swift index 2b0f874961..7e6d6fd2ce 100644 --- a/examples/demo/App/App.swift +++ b/examples/demo/App/App.swift @@ -36,16 +36,9 @@ struct App: SwiftUI.App { var body: some Scene { WindowGroup { - TabView { - ContentView() - .environmentObject(viewModel) - .environmentObject(toastPresenter) - .tabItem { Label("Home", systemImage: "photo.on.rectangle") } - Color.clear - .tabItem { Label("Library", systemImage: "square.stack.fill") } - Color.clear - .tabItem { Label("Search", systemImage: "magnifyingglass") } - } + ContentView() + .environmentObject(viewModel) + .environmentObject(toastPresenter) } } } diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index 88939cd5f2..610c5f3ab2 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -128,14 +128,6 @@ struct ContentView: View { } .accessibilityIdentifier("brand_title") } - ToolbarItemGroup(placement: .navigationBarTrailing) { - Button {} label: { Image(systemName: "chevron.left") } - Button {} label: { Image(systemName: "ellipsis") } - Button {} label: { Image(systemName: "square.and.arrow.up") } - Button {} label: { Image(systemName: "heart") } - Button {} label: { Image(systemName: "bookmark") } - Button {} label: { Image(systemName: "trash") } - } } } From b1f8f27938e6689860a433edcd9f9ed474303497 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 3 Nov 2026 10:19:45 -0800 Subject: [PATCH 13/21] fix(iam): look up reserved regions at runtime Release frameworks are built with an SDK older than iOS 27.1, which compiled the camera clearance out of shipped binaries. Co-Authored-By: Claude Opus 5.5 --- .../UI/OSInAppMessageViewController.m | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 296b007094..10ec36a29d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -34,6 +34,17 @@ #import "OSSessionManager.h" #import "UIApplication+OneSignal.h" +// iOS 27.1 reserved region API, declared here because release binaries are built with an older SDK +@protocol OSReservedRegionKindClass +- (id)occlusionRegionKind; +@end +@protocol OSReservedRegionQuerying +- (NSArray *)reservedRegionsOfKind:(id)kind; +@end +@protocol OSReservedRegion +@property (nonatomic, readonly) CGRect frame; +@end + #define HIGHEST_CONSTRAINT_PRIORITY 999.0f #define HIGH_CONSTRAINT_PRIORITY 990.0f #define MEDIUM_CONSTRAINT_PRIORITY 950.0f @@ -398,10 +409,10 @@ - (UIEdgeInsets)horizontalInsets { UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : sceneView.safeAreaInsets; CGFloat left = MAX(margin, safeArea.left); CGFloat right = MAX(margin, safeArea.right); -#if defined(__IPHONE_27_1) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_27_1 if (@available(iOS 27.1, *)) { CGFloat width = sceneView.bounds.size.width; - for (UIViewReservedRegion *region in [sceneView reservedRegionsOfKind:UIViewReservedRegionKind.occlusionRegionKind]) { + id kind = [(id)NSClassFromString(@"UIViewReservedRegionKind") occlusionRegionKind]; + for (id region in [(id)sceneView reservedRegionsOfKind:kind]) { CGFloat minX = CGRectGetMinX(region.frame); CGFloat maxX = CGRectGetMaxX(region.frame); if (minX >= 1 && maxX <= safeArea.left + 1) { @@ -411,7 +422,6 @@ - (UIEdgeInsets)horizontalInsets { } } } -#endif return UIEdgeInsetsMake(0, left, 0, right); } From 9205fc8ce13300826492e56ad429e45370b17155 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 3 Nov 2026 11:20:47 -0800 Subject: [PATCH 14/21] fix(iam): prefer lowest visible full scene window for insets Co-Authored-By: Claude Opus 5.5 --- .../UI/OSInAppMessageViewController.m | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 10ec36a29d..68fcb6bd89 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -77,6 +77,8 @@ @interface OSInAppMessageViewController () // This recognizer lets the user pan (swipe) the message up and down @property (weak, nonatomic, nullable) UIPanGestureRecognizer *panGestureRecognizer; +@property (nonatomic) BOOL didWarnSceneViewFallback; + // This tap recognizer lets us dismiss the message if the user taps the background @property (weak, nonatomic, nullable) UITapGestureRecognizer *tapGestureRecognizer; @@ -392,11 +394,24 @@ - (void)updateVerticalInsets { - (UIView *)sceneView { // Content loads before this message has a window UIWindowScene *scene = self.view.window.windowScene ?: UIApplication.sharedApplication.keyWindow.windowScene; + // Lowest level wins so the app's own window is preferred over alerts and other overlays + UIWindow *match; for (UIWindow *window in scene.windows) { - if (window != self.view.window && CGRectEqualToRect(window.frame, scene.coordinateSpace.bounds)) { - return window; + if (window == self.view.window || window.hidden || !CGRectEqualToRect(window.frame, scene.coordinateSpace.bounds)) { + continue; + } + if (!match || window.windowLevel < match.windowLevel) { + match = window; } } + if (match) { + return match; + } + // Runs on every layout pass, so warn only once per message + if (!self.didWarnSceneViewFallback) { + self.didWarnSceneViewFallback = true; + [OneSignalLog onesignalLog:ONE_S_LL_WARN message:@"In-app message found no full screen app window; using its own window's insets"]; + } return self.view; } From af88faa44ee1a02d9b3b1b4aff1bf0f4ad8fbaba Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 3 Nov 2026 11:28:15 -0800 Subject: [PATCH 15/21] refactor(iam): skip reserved regions for full screen messages Co-Authored-By: Claude Opus 5.5 --- .../OneSignalInAppMessages/UI/OSInAppMessageViewController.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 68fcb6bd89..fb05ee361e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -424,6 +424,10 @@ - (UIEdgeInsets)horizontalInsets { UIEdgeInsets safeArea = self.isFullscreen ? UIEdgeInsetsZero : sceneView.safeAreaInsets; CGFloat left = MAX(margin, safeArea.left); CGFloat right = MAX(margin, safeArea.right); + // Full screen content clears hardware itself, using the insets passed through setSafeAreaInsets + if (self.isFullscreen) { + return UIEdgeInsetsMake(0, left, 0, right); + } if (@available(iOS 27.1, *)) { CGFloat width = sceneView.bounds.size.width; id kind = [(id)NSClassFromString(@"UIViewReservedRegionKind") occlusionRegionKind]; From 2333383125250e8611bf5d8e649e13de92c32f3b Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 3 Nov 2026 11:33:48 -0800 Subject: [PATCH 16/21] fix(iam): only refresh status bar for full screen messages Co-Authored-By: Claude Opus 5.5 --- .../OneSignalInAppMessages/UI/OSInAppMessageViewController.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index fb05ee361e..2918f9df51 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -162,7 +162,9 @@ - (void)viewWillAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // Resizable scenes (e.g. an unfolded display) don't re-query prefersStatusBarHidden when this window becomes key - [self setNeedsStatusBarAppearanceUpdate]; + if (self.isFullscreen) { + [self setNeedsStatusBarAppearanceUpdate]; + } } - (void)viewWillDisappear:(BOOL)animated { From 7827c0a9f827f974927ba33456880f4b6210bbb0 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 3 Nov 2026 11:43:10 -0800 Subject: [PATCH 17/21] chore(iam): warn when display bounds fall back to the screen Co-Authored-By: Claude Opus 5.5 --- .../OneSignalInAppMessages/UIApplication+OneSignal.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m index 6f7a1e9787..d889a9587f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UIApplication+OneSignal.m @@ -28,6 +28,7 @@ #import "UIApplication+OneSignal.h" #import #import +#import @implementation UIApplication (OneSignal) @@ -46,6 +47,7 @@ + (CGRect)displayBounds { if (scene) { return scene.coordinateSpace.bounds; } + [OneSignalLog onesignalLog:ONE_S_LL_WARN message:@"No key window scene; sizing in-app message to the screen, which may not match the app's window"]; return UIScreen.mainScreen.bounds; } From 52f03983362a4aa715bc5c6e5b2ebbe2852f4844 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 3 Nov 2026 11:50:32 -0800 Subject: [PATCH 18/21] fix(iam): re-show message when JS height check fails on rotation Also keep the message view's insets in sync on layout, log the chosen scene window and insets, and note that region frames include margins. Co-Authored-By: Claude Opus 5.5 --- .../OneSignalInAppMessages/UI/OSInAppMessageView.m | 3 +++ .../UI/OSInAppMessageViewController.m | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m index 263fa67204..f06db7e765 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m @@ -155,6 +155,9 @@ - (void)resetWebViewToMaxBoundsAndResizeHeight:(void (^) (NSNumber *newHeight)) if (error) { NSString *errorMessage = [NSString stringWithFormat:@"Javascript Method: %@ Evaluated with Error: %@", OS_JS_GET_PAGE_META_DATA_METHOD, error]; [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:errorMessage]; + // Still complete so a rotation or fold re-shows the message at its previous height + [self setupWebViewConstraints]; + completion(nil); return; } NSString *successMessage = [NSString stringWithFormat:@"Javascript Method: %@ Evaluated with Success: %@", OS_JS_GET_PAGE_META_DATA_METHOD, result]; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 2918f9df51..c542f19d38 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -420,6 +420,7 @@ - (UIView *)sceneView { // Each side uses the larger of the margin and its safe area inset, and on iOS 27.1+ keeps the margin // from hardware inside a side safe area (e.g. a foldable's camera), matching system notification banners. // Regions flush with the edge, like a status bar column, are already covered by the safe area. +// Region frames already include UIKit's interactive margins; the message margin is added on top. - (UIEdgeInsets)horizontalInsets { UIView *sceneView = [self sceneView]; CGFloat margin = self.useWidthMargin ? [OneSignalCoreHelper sizeToScale:MESSAGE_MARGIN] : 0; @@ -451,6 +452,7 @@ - (void)updateHorizontalInsets { return; } UIEdgeInsets insets = [self horizontalInsets]; + self.messageView.horizontalInsets = insets; // Only assign changes, since this runs on every layout pass if (self.leftConstraint.constant != insets.left) { self.leftConstraint.constant = insets.left; @@ -505,6 +507,11 @@ - (void)addConstraintsForMessage { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:screenScale]; let heightMessage = [NSString stringWithFormat:@"In App Message Height: %f", self.message.height.doubleValue]; [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:heightMessage]; + UIView *sceneView = [self sceneView]; + let layoutMessage = [NSString stringWithFormat:@"In App Message layout: scene view %@ (level %.0f) bounds %@ safe area %@ horizontal insets %@", + NSStringFromClass(sceneView.class), sceneView.window.windowLevel, NSStringFromCGRect(sceneView.bounds), + NSStringFromUIEdgeInsets(sceneView.safeAreaInsets), NSStringFromUIEdgeInsets([self horizontalInsets])]; + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:layoutMessage]; // Height constraint based on the IAM being full screen or any others with a specific height // NOTE: full screen IAM payload has no height, so match screen height minus margins @@ -984,8 +991,10 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id Date: Tue, 3 Nov 2026 11:54:26 -0800 Subject: [PATCH 19/21] chore(iam): note when the reserved region shim can go Co-Authored-By: Claude Opus 5.5 --- .../OneSignalInAppMessages/UI/OSInAppMessageViewController.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index c542f19d38..54d99db294 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -34,7 +34,8 @@ #import "OSSessionManager.h" #import "UIApplication+OneSignal.h" -// iOS 27.1 reserved region API, declared here because release binaries are built with an older SDK +// iOS 27.1 reserved region API, declared here because release binaries are built with an older SDK. +// Remove these and call the UIKit API directly once releases build with the iOS 27.1 SDK. @protocol OSReservedRegionKindClass - (id)occlusionRegionKind; @end From eaa970cde03dfcd6c58a42d3806e229afd39edc2 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 4 Nov 2026 11:56:45 -0800 Subject: [PATCH 20/21] fix(iam): send full screen insets from the message's own window The HTML is given the app window's insets before the message window exists, and a page resize doesn't always follow to correct them. Co-Authored-By: Claude Opus 5.5 --- .../UI/OSInAppMessageView.h | 2 +- .../UI/OSInAppMessageView.m | 9 ++--- .../UI/OSInAppMessageViewController.m | 34 ++++++++++++++----- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h index b21e221f7b..6aee779c56 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.h @@ -46,7 +46,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype _Nonnull)initWithMessage:(OSInAppMessageInternal *)inAppMessage withScriptMessageHandler:(id)messageHandler; - (void)resetWebViewToMaxBoundsAndResizeHeight:(void (^) (NSNumber *newHeight)) completion; -- (void)updateSafeAreaInsets; +- (void)updateSafeAreaInsets:(UIEdgeInsets)insets; - (void)setupWebViewConstraints; - (void)loadReplacementURL:(NSURL *)url; - (void)loadedHtmlContent:(NSString *)html withBaseURL:(NSURL *)url; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m index f06db7e765..7f5e4f82aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageView.m @@ -171,13 +171,8 @@ - (void)resetWebViewToMaxBoundsAndResizeHeight:(void (^) (NSNumber *newHeight)) }]; } -- (void)updateSafeAreaInsets { - UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; - CGFloat top = keyWindow.safeAreaInsets.top; - CGFloat bottom = keyWindow.safeAreaInsets.bottom; - CGFloat right = keyWindow.safeAreaInsets.right; - CGFloat left = keyWindow.safeAreaInsets.left; - NSString *safeAreaInsetsObjectString = [NSString stringWithFormat:OS_JS_SAFE_AREA_INSETS_OBJ,top, bottom, right, left]; +- (void)updateSafeAreaInsets:(UIEdgeInsets)insets { + NSString *safeAreaInsetsObjectString = [NSString stringWithFormat:OS_JS_SAFE_AREA_INSETS_OBJ, insets.top, insets.bottom, insets.right, insets.left]; NSString *setInsetsString = [NSString stringWithFormat:OS_SET_SAFE_AREA_INSETS_METHOD, safeAreaInsetsObjectString]; [self.webView evaluateJavaScript:setInsetsString completionHandler:^(NSDictionary *result, NSError * _Nullable error) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m index 54d99db294..14dd14141c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/UI/OSInAppMessageViewController.m @@ -168,6 +168,14 @@ - (void)viewDidAppear:(BOOL)animated { } } +// The HTML was given the app's insets, and a page resize doesn't always follow to correct them +- (void)viewSafeAreaInsetsDidChange { + [super viewSafeAreaInsetsDidChange]; + if (self.isFullscreen && self.didPageRenderingComplete) { + [self updateContentInsets]; + } +} + - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; @@ -326,15 +334,12 @@ - (void)parseContentData:(NSDictionary *)data { - (NSString *)setContentInsetsInHTML:(NSString *)html { NSMutableString *newHTML = [[NSMutableString alloc] initWithString:html]; - UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; - if (!keyWindow) { + UIWindow *window = [self contentInsetsWindow]; + if (!window) { return newHTML; } - CGFloat top = keyWindow.safeAreaInsets.top; - CGFloat bottom = keyWindow.safeAreaInsets.bottom; - CGFloat right = keyWindow.safeAreaInsets.right; - CGFloat left = keyWindow.safeAreaInsets.left; - NSString *safeAreaInsetsObjectString = [NSString stringWithFormat:OS_JS_SAFE_AREA_INSETS_OBJ,top, bottom, right, left]; + UIEdgeInsets insets = window.safeAreaInsets; + NSString *safeAreaInsetsObjectString = [NSString stringWithFormat:OS_JS_SAFE_AREA_INSETS_OBJ, insets.top, insets.bottom, insets.right, insets.left]; NSString *insetsString = [NSString stringWithFormat:@"\n\n\