skip to Main Content

I want to detect device orientation in swift so that I can apply the correct orientation to a video. The code I have for updating device orientation works correctly but is extremely sensitive to tiny movements. Holding the device upward and barely tilting it causes the device orientation to update to a landscape orientation even though the phone is basically still portrait. Is there a way to lower the sensitivity for device orientation so that tiny movements dont cause an unwanted update?

struct TopVideo: View {
    var body: some View {
        ZStack {

        }
        .onRotate { newOrientation in
            withAnimation(.easeInOut(duration: 0.2)){
                orien = newOrientation
            }
        }
    }
}

struct DeviceRotationViewModifier: ViewModifier {
    let action: (UIDeviceOrientation) -> Void

    func body(content: Content) -> some View {
        content
            .onAppear()
            .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                action(UIDevice.current.orientation)
            }
    }
}

extension View {
    func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View {
        self.modifier(DeviceRotationViewModifier(action: action))
    }
}

2

Answers


  1. To expand on matt‘s comment to watch the app orientation instead of the device orientation, you can modify your approach to observe changes in the interface orientation.
    That would focus on the orientation of the app’s user interface, which might change less frequently than the device’s physical orientation, thus providing a less sensitive detection mechanism.

    Modify your DeviceRotationViewModifier to observe changes in the statusBarOrientation of the app’s window scene. That requires access to the current UIWindowScene to observe its interfaceOrientation.

    Make sure your onRotate modifier now triggers actions based on the app’s interface orientation.

    import SwiftUI
    import UIKit
    
    struct TopVideo: View {
        var body: some View {
            ZStack {
                // Your ZStack content
            }
            .onRotate { newOrientation in
                withAnimation(.easeInOut(duration: 0.2)) {
                    // Update your UI based on the newOrientation
                    // orien = newOrientation
                }
            }
        }
    }
    
    struct AppOrientationViewModifier: ViewModifier {
        let action: (UIInterfaceOrientation) -> Void
    
        func body(content: Content) -> some View {
            content
                .onAppear()
                .onReceive(NotificationCenter.default.publisher(for: UIApplication.didChangeStatusBarOrientationNotification)) { _ in
                    if let windowScene = UIApplication.shared.windows.first?.windowScene {
                        action(windowScene.interfaceOrientation)
                    }
                }
        }
    }
    
    extension View {
        func onRotate(perform action: @escaping (UIInterfaceOrientation) -> Void) -> some View {
            self.modifier(AppOrientationViewModifier(action: action))
        }
    }
    

    The AppOrientationViewModifier now listens for the UIApplication.didChangeStatusBarOrientationNotification notification. When the orientation changes, it retrieves the current UIInterfaceOrientation from the UIWindowScene and triggers the provided action.
    The extension method onRotate is adapted to accept a closure with a UIInterfaceOrientation parameter, reflecting the change to app interface orientation detection.

    Again, this assumes your app uses UIWindowScene (which is standard for apps targeting iOS 13 and later). If you are supporting older versions of iOS, you might need to adjust the implementation to make sure compatibility.


    As noted by lorem ipsum in the comments, you could also use:

    • ViewThatFits, which automatically chooses between multiple views based on which one fits the available space best. This is particularly useful in situations where the layout needs to adapt significantly between portrait and landscape orientations without explicitly tracking the orientation state.

      struct AdaptiveVideoLayout: View {
          var body: some View {
              ViewThatFits {
                  PortraitVideoPlayer() // A custom view optimized for portrait orientation
                  LandscapeVideoPlayer() // A custom view optimized for landscape orientation
              }
          }
      }
      
    • SwiftUI.Layout, introduced in iOS 16, where you can define more complex custom layouts that adapt to the container’s size. It offers a more powerful and flexible way to create adaptive UIs that can respond to changes in orientation, screen size, and other factors.

      struct CustomVideoLayout: View {
          var body: some View {
              Layout {
                  // Define custom layout logic that adapts to the container size
              }
          }
      }
      
    Login or Signup to reply.
  2. You just need to put a GeometryReader around your content, to measure the size of the view. This approach works with iPad split screen too.

    At the simplest level, you could pass a flag like isLandscape to your top-level view. The view can then base its layout on this flag. If you really need to perform some kind of action when the orientation changes, an .onChange callback can be used:

    struct ContentView: View {
    
        private func mainContent(isLandscape: Bool) -> some View {
            Text("(isLandscape ? "Landscape" : "Portrait") orientation")
                .frame(width: isLandscape ? 300 : 200, height: isLandscape ? 200 : 300)
                .background(.yellow)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
    
                // pre iOS 17: .onChange(of: isLandscape) { newVal in
                .onChange(of: isLandscape) { oldVal, newVal in
                    print("orientation changed to (newVal ? "landscape" : "portrait")")
                }
        }
    
        var body: some View {
            GeometryReader { proxy in
                mainContent(isLandscape: proxy.size.height < proxy.size.width)
            }
        }
    }
    

    Be aware the two-thirds split-screen on iPad is pretty close to square on some devices. So instead of passing a boolean flag, it might be more useful to pass the actual view size and maybe the safe area insets too. You might like to use a struct to encapsulate this information. This struct could then provide some read-only computed properties:

    struct ViewSize {
    
        let size: CGSize
        let safeAreaInsets: EdgeInsets
    
        var width: CGFloat {
            size.width
        }
    
        var height: CGFloat {
            size.height
        }
    
        var fullWidth: CGFloat {
            size.width + safeAreaInsets.leading + safeAreaInsets.trailing
        }
    
        var fullHeight: CGFloat {
            size.height + safeAreaInsets.top + safeAreaInsets.bottom
        }
    
        var minDimension: CGFloat {
            min(size.width, size.height)
        }
    
        var maxDimension: CGFloat {
            max(size.width, size.height)
        }
    
        var isLandscape: Bool {
            size.height < size.width
        }
    
        // etc.
    
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search