SwiftUI Repaint View Components on Device Rotation

2020-02-08 03:09发布

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.

标签: swiftui
5条回答
可以哭但决不认输i
2楼-- · 2020-02-08 03:28

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

   // ...

}


查看更多
做自己的国王
3楼-- · 2020-02-08 03:31

@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
    }
}
查看更多
爷的心禁止访问
4楼-- · 2020-02-08 03:31

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.

查看更多
Summer. ? 凉城
5楼-- · 2020-02-08 03:31

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
                        }
                    }
      }
} 
查看更多
Viruses.
6楼-- · 2020-02-08 03:36

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!

查看更多
登录 后发表回答