SwiftUI - Determining Current Device and Orientation

So currently what I have been doing for determining what device type and orientation I am in for SwiftUI is using sizeClasses, usually something like the following:


struct SizeClassView: View {
    @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
    @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass?
    
    var body: some View {
        
        if horizontalSizeClass == .compact && verticalSizeClass == .regular {
            
            Text("iPhone Portrait")
        }
        else if horizontalSizeClass == .regular && verticalSizeClass == .compact {
            
            Text("iPhone Landscape")
        }
        else if horizontalSizeClass == .regular && verticalSizeClass == .regular {
            
            Text("iPad Portrait/Landscape")
        }
    }
}


What I'd like to know is: Is there any better way to do this in SwiftUI? The main problem I'm having with this method is that I can't differentiate between iPad Portrait and Landscape, but at least I can use it to differentiate between iPhone Portrait and iPhone Landscape and iPad... Any help and insight is greatly appreciated!

Answered by vjosullivan in 402837022

Apple strongly discourages the use of "landscape" and "portrait" modes in applications and strongly encourages the use of size classes. That implies that your original method - above - is probably the correct way to go. However, size classes cannot tell you what device you are using.


When using multiple windows on iPad, those windows may have compact widths or heights. Therefore you cannot conclude from size classes that your device is an iPhone in the way that you have done in your original post.


(By the way. Thanks for the size classes code. It was just what I was looking for for my application.)


Regards,

Vince.

Accepted Answer

Ultimately you'll probably have to drop down to UIKit to use the UIDevice API. You can opt to receive notifications when it changes, and can use an environment object to watch these:


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)
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var orientationInfo: OrientationInfo
    
    var body: some View {
        Text("Orientation is '\(orientationInfo.orientation == .portrait ? "portrait" : "landscape")'")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(OrientationInfo())
    }
}
12

Thanks Jim. I guess this will suffice for now, I'll probably continue with sizeClasses for iPhone in general and drop to this UIKit solution for iPad when necessary.

Apple strongly discourages the use of "landscape" and "portrait" modes in applications and strongly encourages the use of size classes. That implies that your original method - above - is probably the correct way to go. However, size classes cannot tell you what device you are using.


When using multiple windows on iPad, those windows may have compact widths or heights. Therefore you cannot conclude from size classes that your device is an iPhone in the way that you have done in your original post.


(By the way. Thanks for the size classes code. It was just what I was looking for for my application.)


Regards,

Vince.

You can check for portrait/landscape like this:

Code Block swift
GeometryReader { geometry in
if geometry.size.height > geometry.size.width {
print("portrait")
} else {
print("landscape")
}

That won't tell you what device you are on.
But, that will work if you want to handle wide windows differently from tall windows on an iPad, too. (Or probably on macOS, too. I haven't tested that, though. That's an exercise left for the reader.)


Combine based version of @Jim's answer:


Code Block swift
final class DeviceOrientation: ObservableObject {
enum Orientation {
        case portrait
        case landscape
    }
    @Published var orientation: Orientation
    private var listener: AnyCancellable?
    init() {
        orientation = UIDevice.current.orientation.isLandscape ? .landscape : .portrait
        listener = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
            .compactMap { ($0.object as? UIDevice)?.orientation }
            .compactMap { deviceOrientation -> Orientation? in
                if deviceOrientation.isPortrait {
                    return .portrait
                } else if deviceOrientation.isLandscape {
                    return .landscape
                } else {
                    return nil
                }
            }
            .assign(to: \.orientation, on: self)
    }
    deinit {
        listener?.cancel()
    }
}


Thank you for the code!!! I just add:

     else if horizontalSizeClass == .compact && verticalSizeClass == .compact {               Text("iPhone compact")

for small devices!

Hello! I found another possible solution. Let me know if it works for you:

import SwiftUI
import Combine

struct OrientationView: View {
        
    @State private var orientation: UIDeviceOrientation? = nil
    @State private var orientationChangePublisher: AnyCancellable?
    
    var body: some View {
        Text("Hello, World!")
            .onAppear {
                orientationChangePublisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
                    .compactMap { notification in
                        UIDevice.current.orientation
                    }
                    .sink { newOrientation in
                        orientation = newOrientation
                        print("isLandscape: \(orientation?.isLandscape ?? false))")
                        print("isPortrait: \(orientation?.isPortrait ?? false))")
                        print("isFlat: \(orientation?.isFlat ?? false))")
                    }
            }
            .onDisappear {
                orientationChangePublisher?.cancel()
            }
    }

}

#Preview {
    OrientationView()
}

Best regards.

As a needed a Combine-free solution, I resorted using some good code form:

https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-device-rotation

But this code has a potential issue with initial state, especially for iPads and navigation, so I introduced a fix:

**@State private var orientation = UIDevice.current.orientation **

instead of:

@State private var orientation = UIDeviceOrientation.unknown

SwiftUI - Determining Current Device and Orientation
 
 
Q