SwiftUI在设备旋转时重新绘制视图组件

SwiftUI在设备旋转时重新绘制视图组件,swiftui,Swiftui,如何在SwiftUI中检测设备旋转并重新绘制视图组件 我有一个@State变量,在第一次出现时初始化为UIScreen.main.bounds.width的值。但当设备方向改变时,该值不会改变。当用户更改设备方向时,我需要重新绘制所有组件。@dfd提供了两个很好的选项,我正在添加第三个选项,这是我使用的选项 在我的例子中,我创建了UIHostingController子类,并在函数viewWillTransition中发布了一个自定义通知 然后,在我的环境模型中,我监听这样的通知,它可以在任何视

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


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

@dfd提供了两个很好的选项,我正在添加第三个选项,这是我使用的选项

在我的例子中,我创建了UIHostingController子类,并在函数viewWillTransition中发布了一个自定义通知

然后,在我的环境模型中,我监听这样的通知,它可以在任何视图中使用

struct ContentView: View {
    @EnvironmentObject var model: Model

    var body: some View {
        Group {
            if model.landscape {
                Text("LANDSCAPE")
            } else {
                Text("PORTRAIT")
            }
        }
    }
}
在SceneDelegate.swift中:

    func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {
        model.environment.toggle()
    }
window.rootViewController=MyUIHostingController(rootView:ContentView().environmentObject(模型(isLandscape:windowScene.interfaceOrientation.isLandscape)))
My UIHostingController子类:

扩展通知。名称{
静态let my_onViewWillTransition=Notification.Name(“MainUIHostingController_viewWillTransition”)
}
类MyUIHostingController:UIHostingController,其中内容:视图{
重写func viewWillTransition(到大小:CGSize,带协调器:UIViewControllerTransitionCoordinator){
NotificationCenter.default.post(名称:.my_onViewWillTransition,对象:nil,用户信息:[“大小”:大小])
super.viewWillTransition(到:大小,带:协调器)
}
}
我的模型是:

类模型:ObservableObject{
@已发布变量:Bool=false
初始化(isLandscape:Bool){
self.scape=isLandscape//初始值
NotificationCenter.default.addObserver(self,选择器:#选择器(onViewWillTransition,通知:)),名称:.my_onViewWillTransition,对象:nil)
}
@objc func onViewWillTransition(通知:通知){
guard let size=notification.userInfo?[“size”]作为?CGSize else{return}
横向=尺寸.宽度>尺寸.高度
}
}

有一个更简单的解决方案,由@kontiki提供,无需通知或与UIKit集成

在SceneDelegate.swift中:

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

最终类模型:ObservableObject{
let objectWillChange=observeObjectPublisher()
var环境:Bool=false{willSet{objectWillChange.send()}
}

净效果是,依赖于
@EnvironmentObject model
的视图将在每次环境变化时重新绘制,无论是旋转、大小变化等。

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

斯威夫特

导入联合收割机
最终类设备:ObservieObject{
@已发布的var isLandscape:Bool=false
}
斯威夫特美术馆

class SceneDelegate:UIResponder,UIWindowSceneDelegate{
变量窗口:UIWindow?
//创建实例
让device=device()//在此处更改
func场景(场景:UIScene,willConnectTo会话:UISceneSession,选项connectionOptions:UIScene.connectionOptions){
// ...
//在此处添加实例作为环境对象
让contentView=contentView().environment(\.managedObjectContext,context).environmentObject(设备)
如果让windowScene=场景为?UIWindowScene{
//在此处读取初始设备方向
device.isLandscape=(windowScene.interfaceOrientation.isLandscape==true)
// ...            
}
}
//增加了此功能以在设备旋转时进行注册
func windowScene(windowScene:UIWindowScene,didUpdate-previousCoordinateSpace:UICoordinateSpace,interfaceOrientation-previousInterfaceOrientation:UIInterfaceOrientation,traitCollection-previousTraitCollection:UITracitCollection){
device.isLandscape.toggle()
}
// ...
}

我认为添加

@Environment(\.verticalSizeClass) var sizeClass
查看结构

我有这样的例子:

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)

        }

    }
}
如您所见,我需要重新计算tabBarHeight,以便在额外视图上应用正确的底部填充,添加此属性似乎可以正确触发重新绘制


只需一行代码我尝试了前面的一些答案,但遇到了一些问题。其中一个解决方案可以在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
                        }
                    }
      }
} 

我想知道SwiftUI中是否有一个简单的解决方案可以与任何封闭视图一起工作,以便它可以确定不同的横向/纵向布局。正如@dfd GeometryReader简要提到的,可以使用它来触发更新

请注意,在使用标准尺寸等级/特征不能提供足够信息来实施设计的特殊情况下,这一点有效。例如,纵向和横向需要不同的布局,但两个方向都会导致从环境返回标准大小类。这种情况发生在最大的设备上,比如最大尺寸的手机和iPad

这是“幼稚”的版本,不起作用

struct RotatingWrapper: View {
     
      var body: some View {
            GeometryReader { geometry in
                if geometry.size.width > geometry.size.height {
                     LandscapeView()
                 }
                 else {
                     PortraitView()
                }
           }
     }
}
下面这个版本是一个可旋转类的变体,它是@reuschj中函数生成器的一个很好的示例,但只是针对我的应用程序需求进行了简化

这确实有效

struct RotatingWrapper: View {
    
    func getIsLandscape(geometry:GeometryProxy) -> Bool {
        return geometry.size.width > geometry.size.height
    }
    
    var body: some View {
        GeometryReader { geometry in
            if self.getIsLandscape(geometry:geometry) {
                Text("Landscape")
            }
            else {
                Text("Portrait").rotationEffect(Angle(degrees:90))
            }
        }
    } 
}
这很有趣,因为我假设一些SwiftUI魔法导致了这个显然很简单的语义变化,从而激活了视图重新渲染

您可以使用它的另一个奇怪技巧是,以这种方式“破解”重新渲染,丢弃使用GeometryProxy的结果并执行设备方向查找。这样就可以使用全方位,在本例中,细节被忽略,结果用于触发简单的纵向和横向选择
struct LandscapeView: View {
    
    var body: some View {
         HStack   {
            Group {
            if  UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft {
                VerticallyCenteredContentView()
            }
                Image("rubric")
                    .resizable()
                              .frame(width:18, height:89)
                              //.border(Color.yellow)
                    .padding([UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft ? .trailing : .leading], 16)
            }
            if  UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
              VerticallyCenteredContentView()
            }
         }.border(Color.pink)
   }
}
...
var model = Model()
...

func windowScene(_ windowScene: UIWindowScene, didUpdate previousCoordinateSpace: UICoordinateSpace, interfaceOrientation previousInterfaceOrientation: UIInterfaceOrientation, traitCollection previousTraitCollection: UITraitCollection) {    
    model.isLandScape = windowScene.interfaceOrientation.isLandscape
}
class Model: ObservableObject {
    @Published var isLandScape: Bool = false
}
struct ContentView: View {
    @EnvironmentObject var model: Model

    var body: some View {
        Group {
            if model.isLandscape {
                Text("LANDSCAPE")
            } else {
                Text("PORTRAIT")
            }
        }
    }
}
struct ContentView: View {
    let cards = ["a", "b", "c", "d", "e"]
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    var body: some View {
        let arrOfTexts = {
            ForEach(cards.indices) { (i) in
                Text(self.cards[i])
            }
        }()
        if (horizontalSizeClass == .compact) {
            return VStack {
                arrOfTexts
            }.erase()
        } else {
            return VStack {
                HStack {
                    arrOfTexts
                }
            }.erase()
        }
    }
}

extension  View {
    func erase() -> AnyView {
        return AnyView(self)
    }
}
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
        }
    }
}
class Orientation: ObservableObject {
        let objectWillChange = ObservableObjectPublisher()

        var isLandScape:Bool = false {
            willSet {
                objectWillChange.send() }
        }

        var cancellable: Cancellable?

        init() {

            cancellable = NotificationCenter.default
                .publisher(for: UIDevice.orientationDidChangeNotification)
                .map() { _ in (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight)}
                .removeDuplicates()
                .assign(to: \.isLandScape, on: self)
        }
    }
// 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")
        }
    }
}
// GlobalStates.swift
import Foundation
import SwiftUI

class GlobalStates: ObservableObject {
    @Published var isLandScape: Bool = false
}



// YourAppNameApp.swift
import SwiftUI

@main
struct YourAppNameApp: App {

    // GlobalStates() is an ObservableObject class
    var globalStates = GlobalStates()

    // Device Orientation
    let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
            .makeConnectable()
            .autoconnect()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(globalStates)
                .onReceive(orientationChanged) { _ in
                    // Set the state for current device rotation
                    if UIDevice.current.orientation.isFlat {
                        // ignore orientation change
                    } else {
                        globalStates.isLandscape = UIDevice.current.orientation.isLandscape
                    }
        }
    }
}

// Now globalStates.isLandscape can be used in any view
// ContentView.swift
import SwiftUI

struct ContentView: View {
    @EnvironmentObject var globalStates: GlobalStates
    var body: some View {
            VStack {
                    if globalStates.isLandscape {
                    // Do something
                        } else {
                    // Do something else
                        }
                    }
      }
} 

import SwiftUI

struct DemoView: View {
    
    @Environment(\.horizontalSizeClass) var hSizeClass
    @Environment(\.verticalSizeClass) var vSizeClass
    
    var body: some View {
        VStack {
            if hSizeClass == .compact && vSizeClass == .regular {
                VStack {
                    Text("Vertical View")
                }
            } else {
                HStack {
                    Text("Horizontal View")
                }
            }
        }
    }
}
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
            }
    }
}
extension UIApplication {
    var currentScene: UIWindowScene? {
        connectedScenes
            .first { $0.activationState == .foregroundActive } as? UIWindowScene
    }
}
guard let scene = UIApplication.shared.currentScene else { return }
import SwiftUI
import Foundation

struct TopView: View {

    var body: some View {
        GeometryReader{
            geo in
            VStack{
                if keepSize(geo: geo) {
                    ChildView()
                }
            }.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
        }.background(Color.red)
    }
    
    func keepSize(geo:GeometryProxy) -> Bool {
        MyScreen.shared.width  = geo.size.width
        MyScreen.shared.height = geo.size.height
        return true
    }
}

class MyScreen:ObservableObject {
    static var shared:MyScreen = MyScreen()
    @Published var width:CGFloat = 0
    @Published var height:CGFloat = 0
}

struct ChildView: View {
    // The presence of this line also allows direct access to up-to-date UIScreen.main.bounds.size.width & .height
    @StateObject var myScreen:MyScreen = MyScreen.shared
    
    var body: some View {
        VStack{
            if myScreen.width > myScreen.height {
                Text("Paysage")
            } else {
                Text("Portrait")
            }
        }
    }
}