SwiftUI 在设备旋转时重绘视图组件
SwiftUI Repaint View Components on Device Rotation
如何在 SwiftUI 中检测设备旋转并重新绘制视图组件?
我有一个 @State 变量在第一次出现时初始化为 UIScreen.main.bounds.width 的值。但是当设备方向改变时,这个值不会改变。当用户改变设备方向时,我需要重新绘制所有组件。
@dfd 提供了两个不错的选择,我正在添加第三个,这是我使用的那个。
在我的例子中,我将 UIHostingController 子类化,并且在函数 viewWillTransition 中,我 post 自定义通知。
然后,在我的环境模型中,我会监听这样的通知,然后可以在任何视图中使用它。
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
}
}
@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() } }
}
最终效果是依赖于 @EnvironmentObject model
的视图将在每次环境变化时重新绘制,无论是旋转、大小变化等。
如果有人也对初始设备方向感兴趣。我是这样做的:
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()
}
// ...
}
我认为添加
可以轻松重绘
@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 上应用正确的底部填充,添加此 属性 似乎可以正确触发重新绘制。
只需一行代码!
我尝试了之前的一些答案,但遇到了一些问题。其中一种解决方案在 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 中是否有适用于任何封闭视图的简单解决方案,因此它可以确定不同的 landscape/portrait 布局。正如@dfd GeometryReader 所简要提到的,可用于触发更新。
请注意,这适用于使用标准尺寸 class/traits 无法提供足够信息来实施设计的特殊场合。例如,纵向和横向需要不同的布局,但两个方向都会导致从环境返回标准尺寸 class。这种情况发生在最大的设备上,例如最大尺寸的手机和 iPad。
这是 'naive' 版本,这不起作用。
struct RotatingWrapper: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > geometry.size.height {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
以下版本是可旋转 class 的变体,它是来自 @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 魔术导致了这个看似简单的语义更改以激活视图重新渲染。
您可以使用它的另一个奇怪的技巧是 'hack' 以这种方式重新渲染,丢弃使用 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,例如子视图中的以下内容,因为一旦顶级视图为 'invalidated'[=15,所有子视图将被检查=]
例如:在 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)
}
}
受@caram解决方案的启发,我从windowScene
中获取了isLandscape
属性
在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")
}
}
}
}
没有通知、委派方法、事件、对 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)
}
}
这是一个基于通知发布者的惯用 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
}
}
}
发布者的输出(上面没有使用,因此 _
作为块参数)如果需要,它的 userInfo
属性 中也包含键 "UIDeviceOrientationRotateAnimatedUserInfoKey"
知道旋转是否应该是动画的。
这是一个抽象,允许您将视图树的任何部分包装在可选的基于方向的行为中,作为奖励,它不依赖于 UIDevice 方向,而是基于 space,这允许它在 swift 预览中工作,并根据您的视图的具体容器为不同的布局提供逻辑:
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"))
这似乎对我有用。然后只需初始化并将 Orientation 实例用作 environmentobject
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)
}
}
我得到了
"Fatal error: No ObservableObject of type SomeType found"
因为我忘了在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")
}
}
}
在 iOS14 中执行此操作的最佳方法:
// 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
}
}
}
}
尝试使用 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. Related Apple's documentation 中找到它。
斯威夫特用户界面 2
这是一个不使用 SceneDelegate
的解决方案(新的 SwiftUI 生命周期中缺少它)。
它还使用当前 window 场景中的 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
}
}
}
也可以使用扩展访问当前window场景:
extension UIApplication {
var currentScene: UIWindowScene? {
connectedScenes
.first { [=11=].activationState == .foregroundActive } as? UIWindowScene
}
}
并像这样使用它:
guard let scene = UIApplication.shared.currentScene else { return }
另一个检测方向变化和 splitView 的 hack。 (灵感来自@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")
}
}
}
}
我已更新 以将其加载到初始视图并使用环境对象使其在全局范围内工作。
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
}
}
}
如何在 SwiftUI 中检测设备旋转并重新绘制视图组件?
我有一个 @State 变量在第一次出现时初始化为 UIScreen.main.bounds.width 的值。但是当设备方向改变时,这个值不会改变。当用户改变设备方向时,我需要重新绘制所有组件。
@dfd 提供了两个不错的选择,我正在添加第三个,这是我使用的那个。
在我的例子中,我将 UIHostingController 子类化,并且在函数 viewWillTransition 中,我 post 自定义通知。
然后,在我的环境模型中,我会监听这样的通知,然后可以在任何视图中使用它。
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
}
}
@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() } }
}
最终效果是依赖于 @EnvironmentObject model
的视图将在每次环境变化时重新绘制,无论是旋转、大小变化等。
如果有人也对初始设备方向感兴趣。我是这样做的:
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()
}
// ...
}
我认为添加
可以轻松重绘@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 上应用正确的底部填充,添加此 属性 似乎可以正确触发重新绘制。
只需一行代码!
我尝试了之前的一些答案,但遇到了一些问题。其中一种解决方案在 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 中是否有适用于任何封闭视图的简单解决方案,因此它可以确定不同的 landscape/portrait 布局。正如@dfd GeometryReader 所简要提到的,可用于触发更新。
请注意,这适用于使用标准尺寸 class/traits 无法提供足够信息来实施设计的特殊场合。例如,纵向和横向需要不同的布局,但两个方向都会导致从环境返回标准尺寸 class。这种情况发生在最大的设备上,例如最大尺寸的手机和 iPad。
这是 'naive' 版本,这不起作用。
struct RotatingWrapper: View {
var body: some View {
GeometryReader { geometry in
if geometry.size.width > geometry.size.height {
LandscapeView()
}
else {
PortraitView()
}
}
}
}
以下版本是可旋转 class 的变体,它是来自 @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 魔术导致了这个看似简单的语义更改以激活视图重新渲染。
您可以使用它的另一个奇怪的技巧是 'hack' 以这种方式重新渲染,丢弃使用 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,例如子视图中的以下内容,因为一旦顶级视图为 'invalidated'[=15,所有子视图将被检查=]
例如:在 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)
}
}
受@caram解决方案的启发,我从windowScene
isLandscape
属性
在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")
}
}
}
}
没有通知、委派方法、事件、对 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)
}
}
这是一个基于通知发布者的惯用 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
}
}
}
发布者的输出(上面没有使用,因此 _
作为块参数)如果需要,它的 userInfo
属性 中也包含键 "UIDeviceOrientationRotateAnimatedUserInfoKey"
知道旋转是否应该是动画的。
这是一个抽象,允许您将视图树的任何部分包装在可选的基于方向的行为中,作为奖励,它不依赖于 UIDevice 方向,而是基于 space,这允许它在 swift 预览中工作,并根据您的视图的具体容器为不同的布局提供逻辑:
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"))
这似乎对我有用。然后只需初始化并将 Orientation 实例用作 environmentobject
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)
}
}
我得到了
"Fatal error: No ObservableObject of type SomeType found"
因为我忘了在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")
}
}
}
在 iOS14 中执行此操作的最佳方法:
// 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
}
}
}
}
尝试使用 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. Related Apple's documentation 中找到它。
斯威夫特用户界面 2
这是一个不使用 SceneDelegate
的解决方案(新的 SwiftUI 生命周期中缺少它)。
它还使用当前 window 场景中的 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
}
}
}
也可以使用扩展访问当前window场景:
extension UIApplication {
var currentScene: UIWindowScene? {
connectedScenes
.first { [=11=].activationState == .foregroundActive } as? UIWindowScene
}
}
并像这样使用它:
guard let scene = UIApplication.shared.currentScene else { return }
另一个检测方向变化和 splitView 的 hack。 (灵感来自@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")
}
}
}
}
我已更新
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
}
}
}