如何在SwiftUI中检测设备旋转并重新绘制视图组件?
当第一个出现时,我有一个@State变量初始化为UIScreen.main.bounds.width的值。但是,当设备方向改变时,该值不会改变。当用户更改设备方向时,我需要重新绘制所有组件。
答案 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)
这是一个不使用 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.swift
,window.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)
尝试使用horizontalSizeClass
和verticalSizeClass
:
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
}
}
}