Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Determining Current Device and Orientation

Tags:

ios

uikit

swiftui

I am trying to detect when the device is on iPad and in Portrait.

Currently I use the UIDevice API in UIKit and use an environment object to watch changes. I use the solution found here - Determining Current Device and Orientation.

However the orientationInfo.orientation is initially always equal to .portrait until rotated into portrait and then back to landscape.

So when doing the following to display the FABView

struct HomeView: View {

@EnvironmentObject var orientationInfo: OrientationInfo
let isPhone = UIDevice.current.userInterfaceIdiom == .phone

    var body: some View {
      ZStack(alignment: .bottom) {
          #if os(iOS)
          if isPhone == false && orientationInfo.orientation == .portrait {
            FABView()
          }
          #endif
      }
    }
}

The view is loaded when the iPad is initially in landscape, but when changing to portrait and back to landscape is then removed. Why is this happening and how can I make sure the view isn't loaded on first load ?

Full Code

struct HomeTab: View {
    
    var body: some View {
        NavigationView {
            HomeView()
                .environmentObject(OrientationInfo())
        }
    }
}

struct HomeView: View {

@EnvironmentObject var orientationInfo: OrientationInfo
let isPhone = UIDevice.current.userInterfaceIdiom == .phone

    var body: some View {
      ZStack(alignment: .bottom) {
          #if os(iOS)
          if isPhone == false && orientationInfo.orientation == .portrait {
            FABView()
          }
          #endif
      }
    }
}

final class OrientationInfo: ObservableObject {
    enum Orientation {
        case portrait
        case landscape
    }
    
    @Published var orientation: Orientation
    
    private var _observer: NSObjectProtocol?
    
    init() {
        // fairly arbitrary starting value for 'flat' orientations
        if UIDevice.current.orientation.isLandscape {
            self.orientation = .landscape
        }
        else {
            self.orientation = .portrait
        }
        
        // unowned self because we unregister before self becomes invalid
        _observer = NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: nil) { [unowned self] note in
            guard let device = note.object as? UIDevice else {
                return
            }
            if device.orientation.isPortrait {
                self.orientation = .portrait
            }
            else if device.orientation.isLandscape {
                self.orientation = .landscape
            }
        }
    }
    
    deinit {
        if let observer = _observer {
            NotificationCenter.default.removeObserver(observer)
        }
    }
}
like image 601
RileyDev Avatar asked Dec 03 '25 16:12

RileyDev


2 Answers

You can use UIDevice.orientationDidChangeNotification for detecting orientation changes but you shouldn't rely on it when the app starts.

UIDevice.current.orientation.isValidInterfaceOrientation will be false at the beginning and therefore both

  • UIDevice.current.orientation.isLandscape

and

  • UIDevice.current.orientation.isPortrait

will return false.

Instead you can use interfaceOrientation from the first window scene:

struct ContentView: View {
    @State private var isPortrait = false
    
    var body: some View {
        Text("isPortrait: \(String(isPortrait))")
            .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                guard let scene = UIApplication.shared.windows.first?.windowScene else { return }
                self.isPortrait = scene.interfaceOrientation.isPortrait
            }
    }
}

Also note that device orientation is not equal to interface orientation. When the device is upside down in portrait mode, device orientation is portrait but interface orientation can be landscape as well.

I think it's better to rely on the interface orientation in your case.

like image 150
pawello2222 Avatar answered Dec 06 '25 06:12

pawello2222


I have created a more SwiftUI like solution for detecting orientation changes, which I tested on Xcode 15.4 with the iOS 17.5 simulator. This solution ensures that your SwiftUI views can dynamically respond to device orientation changes while also providing access to screen size, using EnvironmentKey.

Usage in a SwiftUI View

To use the orientation EnvironmentKey:

struct ContentView: View {

  @Environment(\.orientation) private var orientation

  var body: some View {
    ZStack {
      Color.clear
 
      Text(orientation.isLandscape ? "Landscape" : "Portrait")
    }
  }
}

Setup in the App Entry Point

The setup must be done within the App start:

@main
struct StackOverflowApp: App {

  var body: some Scene {
    WindowGroup {
      GeometryReader { proxy in
        ContentView()
          .environment(\.orientation, UIDevice.current.orientation)
          .environment(\.screenSize, proxy.size)
      }
    }
  }
}

Explanation

  • Environment Keys: The orientation and screenSize keys allow you to access these values throughout your SwiftUI views.
  • GeometryReader: Detects screen size changes, which also trigger orientation updates.

EnvironmentKey Definitions

Define the EnvironmentKey for orientation and screen size:

extension EnvironmentValues {

  var orientation: UIDeviceOrientation {
    get { self[OrientationKey.self] }
    set { self[OrientationKey.self] = newValue }
  }

  var screenSize: CGSize {
    get { self[ScreenSizeKey.self] }
    set { self[ScreenSizeKey.self] = newValue }
  }
}

private struct OrientationKey: EnvironmentKey {
  static let defaultValue = UIDevice.current.orientation
}

private struct ScreenSizeKey: EnvironmentKey {
  static let defaultValue: CGSize = .zero
}

Using the screenSize Environment Key

If you ever need to use the screenSize bonus key, you can do so within any SwiftUI view:

@Environment(\.screenSize) private var screenSize
like image 39
Roland Lariotte Avatar answered Dec 06 '25 08:12

Roland Lariotte