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

时间:2019-08-10 11:17:45

标签: swiftui

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

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

17 个答案:

答案 0 :(得分:15)

这是一个基于通知发布者的惯用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
        }
    }
}

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

答案 1 :(得分:7)

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

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

extension UIApplication {
    var currentScene: UIWindowScene? {
        connectedScenes
            .first { $0.activationState == .foregroundActive } as? UIWindowScene
    }
}

并像这样使用它:

guard let scene = UIApplication.shared.currentScene else { return }

答案 2 :(得分:6)

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

在SceneDelegate.swift中:

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

在Model.swift中:

final class Model: ObservableObject {
    let objectWillChange = ObservableObjectPublisher()

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

最终的效果是,每次{e1}}依赖于视图的视图都会在环境变化时(无论是旋转还是大小变化等)重新绘制。

答案 3 :(得分:6)

@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中:

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

我的UIHostingController子类:

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

}

我的模特:

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 :(得分:3)

受@caram解决方案的启发,我从isLandscape抢夺了windowScene属性

SceneDelegate.swift中,从window.windowScene.interfaceOrientation获取当前方向

...
var model = Model()
...

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

通过这种方式,如果用户从横向模式启动应用,我们将从头开始获得true

这里是Model

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

我们可以按照与@kontiki建议完全相同的方式使用它

struct ContentView: View {
    @EnvironmentObject var model: Model

    var body: some View {
        Group {
            if model.isLandscape {
                Text("LANDSCAPE")
            } else {
                Text("PORTRAIT")
            }
        }
    }
}

答案 5 :(得分:2)

很容易没有通知,委派方法,事件,对SceneDelegate.swiftwindow.windowScene.interfaceOrientation的更改等。 尝试在模拟器和旋转设备中运行它。

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

答案 6 :(得分:2)

这里是一种抽象,它允许您以可选的基于方向的行为包装视图树的任何部分,此外,它不依赖于UIDevice方向,而是基于空间的几何形状,因此允许可以在快速预览中工作,并根据您的视图容器专门针对不同的布局提供逻辑:

struct OrientationView<L: View, P: View> : View {
    let landscape : L
    let portrait : P

    var body: some View {
        GeometryReader { geometry in
            Group {
                if geometry.size.width > geometry.size.height { self.landscape }
                else { self.portrait }
            }.frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }

    init(landscape: L, portrait: P) {
        self.landscape = landscape
        self.portrait = portrait
    }
}

struct OrientationView_Previews: PreviewProvider {
    static var previews: some View {
        OrientationView(landscape: Text("Landscape"), portrait: Text("Portrait"))
            .frame(width: 700, height: 600)
            .background(Color.gray)
    }
}

用法:OrientationView(landscape: Text("Landscape"), portrait: Text("Portrait"))

答案 7 :(得分:1)

在iOS 14中执行此操作的最佳方法:

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

答案 8 :(得分:0)

我认为添加

可以轻松地重新粉刷
@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才能在Extra View上应用正确的底部填充,并且添加此属性似乎可以正确触发重新绘制。

只有一行代码!

答案 9 :(得分:0)

我尝试了一些以前的答案,但是有一些问题。其中一种解决方案可以在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
                        }
                    }
      }
} 

答案 10 :(得分:0)

我想知道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中函数生成器的一个很好的示例,但仅针对我的应用程序需求进行了简化https://github.com/reuschj/RotatableStack/blob/master/Sources/RotatableStack/RotatableStack.swift

这确实有效

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的结果并执行设备方向查找。这样一来,便可以使用所有方向的图像,在此示例中,细节被忽略,结果被用于触发简单的肖像和风景选择或其他任何需要的东西。

enum  Orientation {
    case landscape 
    case portrait 
}

struct RotatingWrapper: View {
   
    func getOrientation(geometry:GeometryProxy) -> Orientation {
        let _  = geometry.size.width > geometry.size.height
        if   UIDevice.current.orientation == UIDeviceOrientation.landscapeLeft || UIDevice.current.orientation == UIDeviceOrientation.landscapeRight {
            return .landscape
        }
        else {
            return .portrait
        }
     }
    
    var body: some View {
       ZStack {
        GeometryReader { geometry in
            if  self.getOrientation(geometry: geometry) == .landscape {
                 LandscapeView()
             }
             else {
                 PortraitView()
            }
        }
        }
     }
    
}

此外,刷新顶层视图后,您可以直接使用DeviceOrientation,例如子视图中的以下内容,因为一旦顶层视图“无效”,所有子视图都将被检查

例如:在LandscapeView()中,我们可以为子视图的水平位置设置适当的格式。

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

答案 11 :(得分:0)

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

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

   // ...

}


答案 12 :(得分:0)

这似乎对我有用。然后只需初始化并将Orientation实例用作环境对象

int arr[1] = {};

答案 13 :(得分:0)

我知道了

“致命错误:未找到SomeType类型的ObservableObject”

因为我忘记了在SceneDelegate.swift中调用contentView.environmentObject(orientationInfo)。这是我的工作版本:

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

答案 14 :(得分:0)

尝试使用horizontalSizeClassverticalSizeClass

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

在此tutorial中找到了它。与苹果公司的documentation相关。

答案 15 :(得分:0)

另一个检测方向变化以及 splitView 的技巧。 (灵感来自@Rocket Garden)

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

答案 16 :(得分:0)

我已更新 https://stackoverflow.com/a/62370919/7139611 以加载它以用于初始视图,并使用 Environment 对象使其在全局范围内工作。

    import SwiftUI

class Orientation: ObservableObject {
    @Published var isLandscape: Bool = UIDevice.current.orientation.isLandscape
}

struct ContentView: View {
    
    @StateObject var orientation = Orientation()
    @State var initialOrientationIsLandScape = false

    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, perform: { _ in
            if initialOrientationIsLandScape {
                initialOrientationIsLandScape = false
            } else {
                orientation.isLandscape = UIDevice.current.orientation.isLandscape
            }
        })
        .onAppear {
            orientation.isLandscape = UIDevice.current.orientation.isLandscape
            initialOrientationIsLandScape = orientation.isLandscape
        }
    }
}