view.frame is the size of your view, not the screen, and it changes with the navigation bar, rotation and layout timing. Read the screen through the view’s window scene, or the window scene’s coordinate space when your app can run in Split View. In SwiftUI, use GeometryReader for layout and the window scene for the physical display.Why this happens
view.frame describes the view, not the device. Its size is decided by whatever contains it — a navigation controller, a tab bar controller, a split view — and it is only final after layout has run. In viewWillAppear: the view often still carries the size it was loaded with, so you read the full window. Later, once the bars have been applied, you read the reduced size. That is the inconsistency you are seeing, and it is not a bug in UIKit.
The screen is a separate object. UIScreen describes the physical display, and its bounds are measured in points in the device’s natural orientation. On iPhone that rectangle stays portrait-shaped even when the interface rotates, and on iPad in Split View it still reports the whole display rather than your app’s window.
So the fix is to pick the object that matches the question you are actually asking: the screen for “how big is the display”, the window scene for “how much space does my app have”.
How to fix it
1. Ask the screen, not the view
If you genuinely need the display size, go through the view’s window scene. This also returns the correct screen on external displays and in multi-scene apps.
import UIKit
let bounds: CGRect
if let screen = view.window?.windowScene?.screen {
bounds = screen.bounds
} else {
bounds = UIScreen.main.bounds // fallback for iOS 15 and earlier
}
let screenWidth = bounds.size.width
let screenHeight = bounds.size.height
UIScreen.main still compiles but is deprecated since iOS 16, so prefer the window-scene route in new code. Note that bounds does not rotate with the interface: in landscape the width is still the short side. If you need the size in the current orientation, take it from the window or scene instead.
2. Use the window scene when your app is not full screen
On iPad, Split View, Slide Over and Stage Manager resize your window without changing the screen. The screen object will happily report the full display and mislead you. Ask the scene:
if let scene = view.window?.windowScene {
let sceneSize = scene.coordinateSpace.bounds.size
print(sceneSize.width, sceneSize.height)
}
view.window?.bounds gives the same answer for the window itself. On iOS 16 and later, windowScene.effectiveGeometry.coordinateSpace.bounds reports the geometry the scene is actually using, which is the value to read when the window can be resized while your app is running.
A small helper keeps this out of your view controllers:
import UIKit
extension UIView {
var screenSize: CGSize? {
window?.windowScene?.screen.bounds.size
}
var sceneSize: CGSize? {
window?.windowScene?.coordinateSpace.bounds.size
}
}
window is nil in viewDidLoad, so call these from viewWillAppear or later.
3. Read after layout, and read again after rotation
Any size you read before layout is a guess. If the value feeds layout or drawing, read it in viewDidLayoutSubviews, where the frame is final:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let size = view.bounds.size
// lay out subviews or update a cached value here
}
For rotation, the size passed to viewWillTransition(to:with:) is the new size of the view, not the screen. Read the scene after the transition settles:
override func viewWillTransition(
to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator
) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil) { _ in
let newSize = self.view.window?.windowScene?.coordinateSpace.bounds.size
print(newSize ?? .zero)
}
}
4. In SwiftUI, GeometryReader measures the view
GeometryReader reports the space it was given, after safe areas and container insets. That is usually what you want for layout, but it is not the device screen.
import SwiftUI
import UIKit
struct ScreenSizeView: View {
var body: some View {
GeometryReader { proxy in
VStack {
Text("View: \(Int(proxy.size.width)) x \(Int(proxy.size.height))")
Text("Screen: \(screenDescription)")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private var screenDescription: String {
guard let screen = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first?.screen else { return "unknown" }
let size = screen.bounds.size
return "\(Int(size.width)) x \(Int(size.height))"
}
}
Add .ignoresSafeArea() to the GeometryReader if you want the full view area rather than the safe area. For layout decisions, prefer GeometryReader, size classes or containerRelativeFrame over the physical screen — the screen tells you nothing useful about how much room a view actually has.
5. Do not use applicationFrame
Older answers suggest [UIScreen mainScreen].applicationFrame to exclude the status bar. It has been deprecated since iOS 9 and it does not understand modern layouts. If you need the usable area, subtract the safe area instead:
let usable = view.bounds.inset(by: view.safeAreaInsets)
For pixel dimensions rather than points, UIScreen.nativeBounds and nativeScale are the properties to use — but they follow the same portrait-oriented rule as bounds.
How to prevent it
- Decide up front whether you need the screen, the scene or the safe area, and name the variable accordingly.
- Never cache a size in
viewDidLoad; the window is not attached yet. - Read sizes in
viewDidLayoutSubviewsor after a transition completes, not inviewWillAppear. - Test on iPad with Split View, Slide Over and Stage Manager, and in both orientations.
- Prefer Auto Layout constraints and SwiftUI layout containers over hard-coded numbers derived from the screen.
FAQ
Why is the width the same in portrait and landscape?
UIScreen.bounds is expressed in the device’s natural orientation, which is portrait on iPhone, so it does not rotate. Read view.window?.windowScene?.coordinateSpace.bounds or the window’s bounds when you need the current orientation.
How do I get the size without the status bar or navigation bar?
Use the safe area rather than the screen: view.safeAreaLayoutGuide.layoutFrame in UIKit, or let GeometryReader sit inside the safe area in SwiftUI. The bars are layout, not hardware.
Is UIScreen.main still safe to use?
It works, but it is deprecated since iOS 16 and it returns the same screen for every scene. Use view.window?.windowScene?.screen instead, which resolves to the correct display for that window.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
