You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Pro 1.6.0 / iOS] vw/vh styles collapse to 0 after the app returns from background — getScreenDimensions() caches a 0×0 sample while the scene is backgrounded #665
After the app spends a while in the background and comes back, every vw/vh-based style resolves to 0: w-[calc(50vw-18px)] buttons shrink to their labels, w-[calc(100vw-24px)] containers collapse so their children pile up in the middle, size-[60vw] images vanish. Styles sized from React Native's useWindowDimensions on the same screens are unaffected, and the layout snaps back on the next device rotation or theme flip. Reported by several production users on iPhone; Android is unaffected.
Mechanism (read from the shipped 1.6.0 sources):
vw compiles to rt.screen.width * N/100, so it is entirely driven by the native runtime's screen.
ios/NativePlatform+ios.swiftgetScreenDimensions() reads RCTPresentedViewController().view.window?.frame and returns Dimensions(width: 0, height: 0) when that is nil ("this should never happen"). getPixelRatio() falls back to 1.0 on the same path.
RCTPresentedViewController() is RCTKeyWindow().rootViewController, and RCTKeyWindow() only accepts a scene that is foregroundActive/foregroundInactive. While the scene is .background it returns nil, so the sample is 0×0.
NativePlatformListener+ios.swift re-samples on UIDevice.orientationDidChangeNotification, RCTWindowFrameDidChangeNotification, RCTUserInterfaceStyleDidChangeNotification, and (iOS 17+) window.registerForTraitChanges([UITraitUserInterfaceStyle.self]). The last one has no app-state guard and calls onNativePlatformChange() directly. After didEnterBackground, iOS flips the window's appearance light↔dark to render the app-switcher snapshots in both schemes (the same mechanism React Native guards against in RCTSurfaceHostingView traitCollectionDidChange: for Appearance addChangeListener handler is called when app goes to background with wrong color scheme react/react-native#28525), so the trait closure fires with the scene backgrounded and the 0×0 runtime is cached. Physical rotation (including face-up/face-down, since react-native-screens starts device-orientation notifications) reaching a backgrounded process does the same through the orientation publisher.
Nothing re-samples on UIApplication.didBecomeActiveNotification, so the zero persists until the next orientation or trait event.
This is the same bug unistyles had in jpudysz/react-native-unistyles#527 ("Runtime is equals to 0 on appState change"), fixed there in #540 by returning the cached last-known screen when the presented view controller is unavailable in the background. Pro 1.6.0 still ships the pre-fix code.
Suggested fix: never persist a nil-window sample — return the cached last-known screen/pixelRatio (or the window scene's screen bounds when there is no cache yet) instead of zeros — and add UIApplication.didBecomeActiveNotification to the merged publishers so anything sampled during a transition is re-checked once the app is active. Optionally skip the iOS 17 trait callback while UIApplication.shared.applicationState == .background, mirroring React Native's guard.
Note for reproduction: an explicit Uniwind.setTheme("dark" | "light") calls Appearance.setColorScheme, which sets overrideUserInterfaceStyle on every window, so the window trait never changes and the trait trigger is disabled. The repro therefore starts with setTheme("system").
Captured on an iPhone 17 Pro simulator (iOS 26.5) with NSLog probes added to getScreenDimensions() and the two listeners, after backgrounding the app, changing the system appearance, and foregrounding it. UIKit's own InterfaceStyle lines are interleaved:
08:09:50.660 [com.apple.UIKit:InterfaceStyle] Scene did update interface style to 1
08:09:50.661 [uniwind-probe] window trait change appState=2 <- UIApplicationStateBackground
08:09:50.661 [uniwind-probe] sample ZERO appState=2 keyWindow=0 presentedVC=0
08:09:50.705 [com.apple.UIKit:Application] Deactivation reason removed ... <- scene becomes foreground-inactive
08:09:50.708 [uniwind-probe] notification RCTUserInterfaceStyleDidChangeNotification appState=1
08:09:50.708 [uniwind-probe] sample ok 402x874 appState=1 keyWindow=1
The deferred trait update lands while the app is still in .background, the iOS 17 trait callback samples 0×0, and the runtime is updated with it. On the simulator React Native's RCTUserInterfaceStyleDidChangeNotification happened to follow 47 ms later, after the scene became foreground-inactive, and re-sampled correctly. That second notification is guarded in RCTSurfaceHostingView to be skipped while applicationState == Background, so whenever React Native's trait update lands before the state transition (which is what users see on devices), nothing heals the 0×0 runtime until the next rotation or theme change.
Steps to Reproduce
Open the vw screen in the linked repro and press setTheme("system").
Press Home.
While backgrounded, run xcrun simctl ui booted appearance dark, then light (on a device: flip appearance in Control Center, or rotate the phone).
Return to the app.
Expected: the w-[100vw] bar stays full width and the on-screen log shows 100vw=<window width>.
Actual: the bar and the w-[50vw] box collapse to 0 while Dimensions.get("window").width on the same line is unchanged. One rotation in the foreground heals it.
What happened?
After the app spends a while in the background and comes back, every
vw/vh-based style resolves to 0:w-[calc(50vw-18px)]buttons shrink to their labels,w-[calc(100vw-24px)]containers collapse so their children pile up in the middle,size-[60vw]images vanish. Styles sized from React Native'suseWindowDimensionson the same screens are unaffected, and the layout snaps back on the next device rotation or theme flip. Reported by several production users on iPhone; Android is unaffected.Mechanism (read from the shipped 1.6.0 sources):
vwcompiles tort.screen.width * N/100, so it is entirely driven by the native runtime'sscreen.ios/NativePlatform+ios.swiftgetScreenDimensions()readsRCTPresentedViewController().view.window?.frameand returnsDimensions(width: 0, height: 0)when that is nil ("this should never happen").getPixelRatio()falls back to1.0on the same path.RCTPresentedViewController()isRCTKeyWindow().rootViewController, andRCTKeyWindow()only accepts a scene that isforegroundActive/foregroundInactive. While the scene is.backgroundit returns nil, so the sample is 0×0.NativePlatformListener+ios.swiftre-samples onUIDevice.orientationDidChangeNotification,RCTWindowFrameDidChangeNotification,RCTUserInterfaceStyleDidChangeNotification, and (iOS 17+)window.registerForTraitChanges([UITraitUserInterfaceStyle.self]). The last one has no app-state guard and callsonNativePlatformChange()directly. AfterdidEnterBackground, iOS flips the window's appearance light↔dark to render the app-switcher snapshots in both schemes (the same mechanism React Native guards against inRCTSurfaceHostingView traitCollectionDidChange:for Appearance addChangeListener handler is called when app goes to background with wrong color scheme react/react-native#28525), so the trait closure fires with the scene backgrounded and the 0×0 runtime is cached. Physical rotation (including face-up/face-down, since react-native-screens starts device-orientation notifications) reaching a backgrounded process does the same through the orientation publisher.UIApplication.didBecomeActiveNotification, so the zero persists until the next orientation or trait event.This is the same bug unistyles had in jpudysz/react-native-unistyles#527 ("Runtime is equals to 0 on appState change"), fixed there in #540 by returning the cached last-known
screenwhen the presented view controller is unavailable in the background. Pro 1.6.0 still ships the pre-fix code.Suggested fix: never persist a nil-window sample — return the cached last-known
screen/pixelRatio(or the window scene's screen bounds when there is no cache yet) instead of zeros — and addUIApplication.didBecomeActiveNotificationto the merged publishers so anything sampled during a transition is re-checked once the app is active. Optionally skip the iOS 17 trait callback whileUIApplication.shared.applicationState == .background, mirroring React Native's guard.Note for reproduction: an explicit
Uniwind.setTheme("dark" | "light")callsAppearance.setColorScheme, which setsoverrideUserInterfaceStyleon every window, so the window trait never changes and the trait trigger is disabled. The repro therefore starts withsetTheme("system").Captured on an iPhone 17 Pro simulator (iOS 26.5) with
NSLogprobes added togetScreenDimensions()and the two listeners, after backgrounding the app, changing the system appearance, and foregrounding it. UIKit's ownInterfaceStylelines are interleaved:The deferred trait update lands while the app is still in
.background, the iOS 17 trait callback samples 0×0, and the runtime is updated with it. On the simulator React Native'sRCTUserInterfaceStyleDidChangeNotificationhappened to follow 47 ms later, after the scene became foreground-inactive, and re-sampled correctly. That second notification is guarded inRCTSurfaceHostingViewto be skipped whileapplicationState == Background, so whenever React Native's trait update lands before the state transition (which is what users see on devices), nothing heals the 0×0 runtime until the next rotation or theme change.Steps to Reproduce
setTheme("system").xcrun simctl ui booted appearance dark, thenlight(on a device: flip appearance in Control Center, or rotate the phone).Expected: the
w-[100vw]bar stays full width and the on-screen log shows100vw=<window width>.Actual: the bar and the
w-[50vw]box collapse to 0 whileDimensions.get("window").widthon the same line is unchanged. One rotation in the foreground heals it.Snack or Repository Link (Optional)
https://github.com/invivek26/uniwind-frozen-swap-repro — screen vw, README section "Third issue", probe patch in
patches/uniwind-pro-1.6.0-probe.patchUniwind version
uniwind-pro 1.6.0
React Native Version
0.85.3 (Expo SDK 56)
Platforms
iOS
Expo
Yes
Additional information 〰