SwiftUI Repaint View Components on Device Rotation

2020-02-08 03:15发布

问题:

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 appears. But this value doesn't change when the device orientation changes. I need to redraw all components when the user changes the device orientation.

回答1:

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


回答2:

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.



回答3:

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

   // ...

}




回答4:

I think easy repainting is possible with addition of

@Environment(\.verticalSizeClass) var sizeClass

to View struct.

I have such example:

struct MainView: View {

    @EnvironmentObject var model: HamburgerMenuModel
    @Environment(\.verticalSizeClass) var sizeClass

    var body: some View {

        let tabBarHeight = UITabBarController().tabBar.frame.height

        return ZStack {
            HamburgerTabView()
            HamburgerExtraView()
                .padding(.bottom, tabBarHeight)

        }

    }
}

As you can see I need to recalculate tabBarHeight to apply correct bottom padding on Extra View, and addition of this property seems to correctly trigger repainting.

With just one line of code!



回答5:

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


标签: swiftui