SwiftUI Repaint View Components on Device Rotation

后端 未结 12 1868
谎友^
谎友^ 2020-12-01 01:00

How to detect device rotation in SwiftUI and re-draw view components?

I have a @State variable initialized to the value of UIScreen.main.bounds.width when the first

相关标签:
12条回答
  • 2020-12-01 01:25

    There is an easier solution that the one provided by @kontiki, with no need for notifications or integration with UIKit.

    In SceneDelegate.swift:

        func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
            model.environment.toggle()
        }
    

    In Model.swift:

    final class Model: ObservableObject {
        let objectWillChange = ObservableObjectPublisher()
    
        var environment: Bool = false { willSet { objectWillChange.send() } }
    }
    

    The net effect is that the views that depend on the @EnvironmentObject model will be redrawn each time the environment changes, be it rotation, changes in size, etc.

    0 讨论(0)
  • 2020-12-01 01:26

    If someone is also interested in the initial device orientation. I did it as follows:

    Device.swift

    import Combine
    
    final class Device: ObservableObject {
        @Published var isLandscape: Bool = false
    }
    

    SceneDelegate.swift

    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
        var window: UIWindow?
    
        // created instance
        let device = Device() // changed here
    
        func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    
            // ...
    
            // added the instance as environment object here
            let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(device) 
    
    
            if let windowScene = scene as? UIWindowScene {
    
                // read the initial device orientation here
                device.isLandscape = (windowScene.interfaceOrientation.isLandscape == true)
    
                // ...            
    
            }
        }
    
        // added this function to register when the device is rotated
        func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
            device.isLandscape.toggle()
        }
    
       // ...
    
    }
    
    
    
    0 讨论(0)
  • 2020-12-01 01:30

    @dfd provided two good options, I am adding a third one, which is the one I use.

    In my case I subclass UIHostingController, and in function viewWillTransition, I post a custom notification.

    Then, in my environment model I listen for such notification which can be then used in any view.

    struct ContentView: View {
        @EnvironmentObject var model: Model
    
        var body: some View {
            Group {
                if model.landscape {
                    Text("LANDSCAPE")
                } else {
                    Text("PORTRAIT")
                }
            }
        }
    }
    

    In SceneDelegate.swift:

    window.rootViewController = MyUIHostingController(rootView: ContentView().environmentObject(Model(isLandscape: windowScene.interfaceOrientation.isLandscape)))
    

    My UIHostingController subclass:

    extension Notification.Name {
        static let my_onViewWillTransition = Notification.Name("MainUIHostingController_viewWillTransition")
    }
    
    class MyUIHostingController<Content> : UIHostingController<Content> where Content : View {
    
        override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
            NotificationCenter.default.post(name: .my_onViewWillTransition, object: nil, userInfo: ["size": size])
            super.viewWillTransition(to: size, with: coordinator)
        }
    
    }
    

    And my model:

    class Model: ObservableObject {
        @Published var landscape: Bool = false
    
        init(isLandscape: Bool) {
            self.landscape = isLandscape // Initial value
            NotificationCenter.default.addObserver(self, selector: #selector(onViewWillTransition(notification:)), name: .my_onViewWillTransition, object: nil)
        }
    
        @objc func onViewWillTransition(notification: Notification) {
            guard let size = notification.userInfo?["size"] as? CGSize else { return }
    
            landscape = size.width > size.height
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:34

    Here‘s an idiomatic SwiftUI implementation based on a notification publisher:

    struct ContentView: View {
        
        @State var orientation = UIDevice.current.orientation
    
        let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
            .makeConnectable()
            .autoconnect()
    
        var body: some View {
            Group {
                if orientation.isLandscape {
                    Text("LANDSCAPE")
                } else {
                    Text("PORTRAIT")
                }
            }.onReceive(orientationChanged) { _ in
                self.orientation = UIDevice.current.orientation
            }
        }
    }
    

    The output of the publisher (not used above, therefor _ as the block parameter) also contains the key "UIDeviceOrientationRotateAnimatedUserInfoKey" in its userInfo property if you need to know if the rotation should be animated.

    0 讨论(0)
  • 2020-12-01 01:35

    I got

    "Fatal error: No ObservableObject of type SomeType found"

    because I forgot to call contentView.environmentObject(orientationInfo) in SceneDelegate.swift. Here is my working version:

    // OrientationInfo.swift
    final class OrientationInfo: ObservableObject {
        @Published var isLandscape = false
    }
    
    // SceneDelegate.swift
    var orientationInfo = OrientationInfo()
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // ...
        window.rootViewController = UIHostingController(rootView: contentView.environmentObject(orientationInfo))
        // ...
    }
    
    func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
        orientationInfo.isLandscape = windowScene.interfaceOrientation.isLandscape
    }
    
    // YourView.swift
    @EnvironmentObject var orientationInfo: OrientationInfo
    
    var body: some View {
        Group {
            if orientationInfo.isLandscape {
                Text("LANDSCAPE")
            } else {
                Text("PORTRAIT")
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-01 01:37

    I tried some of the previous answers, but had a few problems. One of the solutions would work 95% of the time but would screw up the layout every now and again. Other solutions didn't seem to be in tune with SwiftUI's way of doing things. So I came up with my own solution. You might notice that it combines features of several previous suggestions.

    // Device.swift
    import Combine
    import UIKit
    
    final public class Device: ObservableObject {
    
      @Published public var isLandscape: Bool = false
    
    public init() {}
    
    }
    
    //  SceneDelegate.swift
    import SwiftUI
    
    class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
        var window: UIWindow?
        var device = Device()
    
       func scene(_ scene: UIScene, 
            willConnectTo session: UISceneSession, 
            options connectionOptions: UIScene.ConnectionOptions) {
    
            let contentView = ContentView()
                 .environmentObject(device)
            if let windowScene = scene as? UIWindowScene {
            // standard template generated code
            // Yada Yada Yada
    
               let size = windowScene.screen.bounds.size
               device.isLandscape = size.width > size.height
            }
    }
    // more standard template generated code
    // Yada Yada Yada
    func windowScene(_ windowScene: UIWindowScene, 
        didUpdate previousCoordinateSpace: UICoordinateSpace, 
        interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, 
        traitCollection previousTraitCollection: UITraitCollection) {
    
        let size = windowScene.screen.bounds.size
        device.isLandscape = size.width > size.height
    }
    // the rest of the file
    
    // ContentView.swift
    import SwiftUI
    
    struct ContentView: View {
        @EnvironmentObject var device : Device
        var body: some View {
                VStack {
                        if self.device.isLandscape {
                        // Do something
                            } else {
                        // Do something else
                            }
                        }
          }
    } 
    
    0 讨论(0)
提交回复
热议问题