设备旋转时的SwiftUI重绘视图组件

ora*_*ode 3 swiftui

如何在SwiftUI中检测设备旋转并重新绘制视图组件?

当第一个出现时,我有一个@State变量初始化为UIScreen.main.bounds.width的值。但是,当设备方向改变时,该值不会改变。当用户更改设备方向时,我需要重新绘制所有组件。

Kor*_*tor 25

这是一个基于通知发布者的惯用 SwiftUI 实现:

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您需要知道旋转是否应该动画,则发布者的输出(上面未使用,因此_作为块参数)还包含"UIDeviceOrientationRotateAnimatedUserInfoKey"userInfo属性中的键。

  • 这确实对我有用,但它似乎无法读取设备的初始方向。有任何想法吗? (2认同)
  • 与@benpomeroy9 相同的问题:未正确读取初始方向。尝试在 `init` 或 `onAppear` 中设置它无济于事...... (2认同)

sim*_*bac 8

如果有人也对初始设备方向感兴趣。我是这样做的:

设备.swift

import Combine

final class Device: ObservableObject {
    @Published var isLandscape: Bool = false
}
Run Code Online (Sandbox Code Playgroud)

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

   // ...

}


Run Code Online (Sandbox Code Playgroud)


paw*_*222 8

SwiftUI 2

这是一个未使用SceneDelegate(在新的 SwiftUI 生命周期中缺少)的解决方案。

它还使用interfaceOrientation来自当前窗口场景而不是 UIDevice.current.orientation(在应用程序启动时未设置)。

这是一个演示:

struct ContentView: View {
    @State private var isPortrait = false
    
    var body: some View {
        Text("isPortrait: \(String(isPortrait))")
            .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                guard let scene = UIApplication.shared.windows.first?.windowScene else { return }
                self.isPortrait = scene.interfaceOrientation.isPortrait
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

也可以使用扩展来访问当前窗口场景:

extension UIApplication {
    var currentScene: UIWindowScene? {
        connectedScenes
            .first { $0.activationState == .foregroundActive } as? UIWindowScene
    }
}
Run Code Online (Sandbox Code Playgroud)

并像这样使用它:

guard let scene = UIApplication.shared.currentScene else { return }
Run Code Online (Sandbox Code Playgroud)


Mic*_*bro 7

我认为通过添加可以轻松重新粉刷

@Environment(\.verticalSizeClass) var sizeClass
Run Code Online (Sandbox Code Playgroud)

查看结构。

我有这样的例子:

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)

        }

    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,我需要重新计算 tabBarHeight 以在 Extra View 上应用正确的底部填充,并且添加此属性似乎可以正确触发重绘。

只需一行代码!

  • 仅适用于 iPhone 不是吗?由于 SizeClass 在水平或垂直方向上都不会改变...... (3认同)

kon*_*iki 5

@dfd提供了两个不错的选择,我添加了第三个选择,这是我使用的一个。

就我而言,我继承了UIHostingController的子类,并在函数viewWillTransition中发布了自定义通知。

然后,在我的环境模型中,我侦听此类通知,然后可以在任何视图中使用。

struct ContentView: View {
    @EnvironmentObject var model: Model

    var body: some View {
        Group {
            if model.landscape {
                Text("LANDSCAPE")
            } else {
                Text("PORTRAIT")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在SceneDelegate.swift中:

struct ContentView: View {
    @EnvironmentObject var model: Model

    var body: some View {
        Group {
            if model.landscape {
                Text("LANDSCAPE")
            } else {
                Text("PORTRAIT")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我的UIHostingController子类:

window.rootViewController = MyUIHostingController(rootView: ContentView().environmentObject(Model(isLandscape: windowScene.interfaceOrientation.isLandscape)))
Run Code Online (Sandbox Code Playgroud)

而我的模型:

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

}
Run Code Online (Sandbox Code Playgroud)


car*_*ram 5

@kontiki提供了一种更简单的解决方案,无需通知或与UIKit集成。

在SceneDelegate.swift中:

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

在Model.swift中:

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

最终结果是,@EnvironmentObject model每次环境更改时(无论是旋转还是大小更改),都将重绘依赖的视图。


Geo*_*rns 5

我尝试了之前的一些答案,但遇到了一些问题。其中一种解决方案在 95% 的时间内都可以工作,但时不时就会搞乱布局。其他解决方案似乎与 SwiftUI 的工作方式不一致。所以我想出了自己的解决方案。您可能会注意到它结合了之前几个建议的功能。

// 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
                        }
                    }
      }
} 
Run Code Online (Sandbox Code Playgroud)