From 272a8c10c35ddaadb6f25b4c96805cd80b37044c Mon Sep 17 00:00:00 2001 From: Ognjen Ristanovic Date: Wed, 7 Oct 2026 17:37:58 +0200 Subject: [PATCH] Size embedded content against the view's own box MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An embedded view that React Native gives no height resolved its percent placement against the full screen instead. The scene then rendered at screen height inside a zero-height box and painted over whatever the app had drawn around it. The size fell back to the key window's bounds whenever the measured width or height was zero. That fallback reads a zero measurement as "unknown", but under Yoga an unsized view really is zero, so the stand-in replaced a real answer rather than filling in a missing one. AirshipEmbeddedSize takes optionals, and nil is the SDK's own signal for "no parent stated" — it then resolves against the SwiftUI proposal, which is the actual box. So pass nil instead of a stand-in. Clip the host view as well. Scene content reports its own size and can exceed the box regardless of what the parent size says, and a UIView paints outside its bounds by default. Co-Authored-By: Claude Opus 5 --- ios/AirshipEmbeddedViewWrapper.swift | 37 +++++++++++----------------- ios/RNAirshipEmbeddedView.mm | 3 +++ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/ios/AirshipEmbeddedViewWrapper.swift b/ios/AirshipEmbeddedViewWrapper.swift index 90e51b41..8f34f186 100644 --- a/ios/AirshipEmbeddedViewWrapper.swift +++ b/ios/AirshipEmbeddedViewWrapper.swift @@ -110,8 +110,8 @@ struct ReactAirshipEmbeddedView: View { if let embeddedID = viewModel.embeddedID { AirshipEmbeddedView(embeddedID: embeddedID, embeddedSize: .init( - parentWidth: viewModel.width, - parentHeight: viewModel.height + parentWidth: viewModel.parentWidth, + parentHeight: viewModel.parentHeight ), selection: viewModel.selection ) @@ -124,27 +124,18 @@ struct ReactAirshipEmbeddedView: View { @Published var size: CGSize? @Published var selection: AirshipEmbeddedSelection = .priority - var height: CGFloat { - guard let height = self.size?.height, height > 0 else { - return Self.defaultScreenBounds.height - } - return height - } - - var width: CGFloat { - guard let width = self.size?.width, width > 0 else { - return Self.defaultScreenBounds.width - } - return width - } - - @MainActor - private static var defaultScreenBounds: CGRect { - let window = UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene } - .flatMap { $0.windows } - .first { $0.isKeyWindow } ?? UIApplication.shared.delegate?.window ?? nil - return window?.screen.bounds ?? CGRect(x: 0, y: 0, width: 500, height: 500) + /// The host's bound on each axis, or nil while layout has not supplied one. + /// + /// Nil is the SDK's own signal for "no parent stated": it resolves a percent + /// placement against the SwiftUI proposal instead, which is the real box. A + /// stand-in value overrides that and sizes content against space the app + /// never gave the view. In React Native an unsized view is 0, not unknown. + var parentWidth: CGFloat? { Self.bound(self.size?.width) } + var parentHeight: CGFloat? { Self.bound(self.size?.height) } + + private static func bound(_ length: CGFloat?) -> CGFloat? { + guard let length, length > 0 else { return nil } + return length } } diff --git a/ios/RNAirshipEmbeddedView.mm b/ios/RNAirshipEmbeddedView.mm index 0ea6732c..686ac452 100644 --- a/ios/RNAirshipEmbeddedView.mm +++ b/ios/RNAirshipEmbeddedView.mm @@ -55,6 +55,9 @@ - (void)unmountChildComponentView:(UIView *)childCompo - (instancetype) init { self = [self initWithFrame:CGRectZero]; if (self) { + // Scene content reports its own size, which can exceed the box Yoga assigned. + // Unclipped, a UIView paints that overflow over the app's own views. + self.clipsToBounds = YES; self.wrapper = [[RNAirshipEmbeddedViewBridgeClass() alloc] initWithFrame:self.bounds]; [self addSubview:self.wrapper]; }