环境: 使用 Swift 5.3 的 SwiftUI
场景: 根据 Xcode 常规设置,默认方向为 Portrait、LandscapeLeft 和 LandscapeRight。
这允许可能按需显示横向,而不是仅将 Xcode 设置为纵向。
该项目正在使用 SwiftUI Lifecycle 与 AppDelegate。
目标: 只有特定的视图能够旋转到横向;大多数锁定在肖像。
当前操作方式: 设备在当前视图的 .upAppear{} 中设置为仅纵向模式,并通过 onReceive{} 通过 设备方向更改通知。
我发现这是真正进行瞬时肖像锁定的唯一方法,允许其他人渲染风景。
问题:方向更改通知发生得太晚了:我看到实际的景观正在实时修正 - 因此图像在旋转过程中会弹回。
问题: 如何在纵向模式下锁定特定的 swiftUI 视图,以允许其他人自由改变方向?
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
ZStack {
Color.blue
NavigationView {
Text("Hello, world!")
.padding()
.navigationTitle("Turkey Gizzards")
.navigationBarTitleDisplayMode(.inline)
}
}.onAppear {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}
}
}
没有原生 SwiftUI 方法可以做到这一点。目前看来,必须使用 AppDelegate 适配器。
在您的应用程序主目录中,添加以下内容:
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
然后添加这个类:
class AppDelegate: NSObject, UIApplicationDelegate {
static var orientationLock = UIInterfaceOrientationMask.all //By default you want all your views to rotate freely
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
}
在设计为锁定纵向模式的特定视图中:
struct ContentView: View {
var body: some View {
ZStack {
Color.blue
NavigationView {
Text("Hello, world!")
.padding()
.navigationTitle("Turkey Gizzards")
.navigationBarTitleDisplayMode(.inline)
}
}.onAppear {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") // Forcing the rotation to portrait
AppDelegate.orientationLock = .portrait // And making sure it stays that way
}.onDisappear {
AppDelegate.orientationLock = .all // Unlocking the rotation when leaving the view
}
}
}
您还可以根据您的需要,在 onDisappear 内添加另一个
UIDevice.current.setValue(UIInterfaceOrientation.yourOrientation.rawValue, forKey: "orientation")
,以在离开视图时强制旋转。
您在使用
UIHostingController
吗?另一种解决方法可能是将其子类化以实现 supportedInterfaceOrientations
/ shouldAutorotate
,就像我们通常在 UIKit 中所做的那样:
class HostingController<Content>: UIHostingController<Content> where Content: View {}
提供的解决方案在 iOS 16 上不起作用,我清理了 SwiftUI 实现。
我的解决方案旋转到正确的方向并锁定旋转。默认为纵向,并在特定视图上强制横向。这可以根据您的需要进行更改。
在
AppDelegate
中添加:
static var orientationLock = UIInterfaceOrientationMask.portrait {
didSet {
if #available(iOS 16.0, *) {
UIApplication.shared.connectedScenes.forEach { scene in
if let windowScene = scene as? UIWindowScene {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: orientationLock))
}
}
UIViewController.attemptRotationToDeviceOrientation()
} else {
if orientationLock == .landscape {
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
} else {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
}
}
}
}
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
创建一个
View+Extensions
并添加以下代码:
extension View {
@ViewBuilder
func forceRotation(orientation: UIInterfaceOrientationMask) -> some View {
self.onAppear() {
AppDelegate.orientationLock = orientation
}
// Reset orientation to previous setting
let currentOrientation = AppDelegate.orientationLock
self.onDisappear() {
AppDelegate.orientationLock = currentOrientation
}
}
}
然后在 SwiftUI 中你可以这样做:
struct DummyView: View {
var body: some View {
ZStack {
Text("Dummy View")
}.forceRotation(orientation: .landscape)
}
}
因此,在 SwiftUI 中,Apple 试图强制您仅使用 Info.plist 和 Xcode General Deployment Info 来强制视图方向。它们不支持将 SwiftUI 视图锁定到特定方向,这很荒谬,尤其是在您使用相机时。他们还不断弃用方向变量。因此,解决这些 AppHole 问题很复杂,但下面的代码可以工作。
在AppDelegate中有这个。
static var orientationLock = UIInterfaceOrientationMask.all
static var orientationLock = UIInterfaceOrientationMask.all
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
现在您需要一个 SwiftUI 视图扩展
extension View {
func detectOrientation(_ binding: Binding<UIDeviceOrientation>) -> some View {
self.modifier(OrientationDetector(orientation: binding))
}
}
struct OrientationDetector: ViewModifier {
@Binding var orientation: UIDeviceOrientation
let orientationMask = AppDelegate.orientationLock
func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
if checkOrientation(orientationMask: orientationMask)
{
orientation = UIDevice.current.orientation
}
else
{
orientation = AppDelegate.deviceOrientation
}
}
}
}
现在它变得棘手,因为 UIDevice.current.orientation 有 .faceUp 和 .faceDown 你不关心,所以你需要忽略这些视图方向,这实际上是 checkOrieration() 方法通过保存先前的方向来完成的事情确实进入 .faceUp 或 .faceDown。
func checkOrientation(orientationMask:UIInterfaceOrientationMask) -> Bool
{
//Here we're ignoring faceUp and faceDown and storing the prior orientation in AppDelegate.deviceOrientation
//This effectively eliminates these two problematic orientations so the App will ignore these and return the prior value
var status : Bool = false
switch UIDevice.current.orientation {
case .portrait :
if(orientationMask == .portrait || orientationMask == .portraitUpsideDown || orientationMask == .allButUpsideDown)
{
status = true
AppDelegate.deviceOrientation = UIDevice.current.orientation
}
case .landscapeLeft :
if orientationMask == .landscapeLeft || orientationMask == .allButUpsideDown
{
status = true
AppDelegate.deviceOrientation = UIDevice.current.orientation
}
case .landscapeRight :
if orientationMask == .landscapeRight || orientationMask == .allButUpsideDown
{
status = true
AppDelegate.deviceOrientation = UIDevice.current.orientation
}
case .portraitUpsideDown :
if orientationMask == .portraitUpsideDown || orientationMask == .allButUpsideDown
{
status = true
AppDelegate.deviceOrientation = UIDevice.current.orientation
}
case .faceUp :
status = false
case .faceDown :
status = false
case .unknown:
status = false
@unknown default:
status = false
}
return status
}
现在在您的主 SwiftUI 视图中,您需要一个 init 和 .detectOrination($orientation)
@State private var orientation = UIDevice.current.orientation
init() {
//Lock into all view rotations
AppDelegate.orientationLock = .allButUpsideDown
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .allButUpsideDown))
UIApplication.navigationTopViewController()?.setNeedsUpdateOfSupportedInterfaceOrientations()
}
}
//At bottom of SwiftUI view add this :
.detectOrientation($orientation)
上面对 SwiftUI 视图所做的就是让它旋转 .allButUpsideDown 视图旋转。如果你想锁定它,请设置.portrait,或.landscapeLeft或.landscapeRight。
主 SwiftUI 视图将声明 @State var 方向,并且所有需要方向的 SwiftUI 子视图都需要使用相同的变量。
因此 SwiftUI 子视图需要这个:
@Binding private var orientation:UIDeviceOrientation
init(orientation:Binding<UIDeviceOrientation>)
self._orientation = orientation
}
现在,在主视图和子视图中,您可以执行此操作来处理特定视图方向的布局。
if orientation.isLandscape {
//SwiftUI elements for Landscape
} else {
//SwiftUI elements for Portrait
}
就是这样,需要很多代码来解决这个问题,但它可以在 SwiftUI 版本 5 和 iOS 16+ 中运行,Apple 也会批准它用于应用程序版本。
我们确实需要游说苹果支持将 SwiftUI 视图本地锁定到特定的视图方向,这可以通过调用一个简单的 SwiftUI 方法来完成。他们还需要为 UIDeviceOrientation 创建一个新的 var,忽略 .faceUp 和 .faceDown,以便您可以在 SWiftUI 视图中使用此 var 并相应地布局您的视图元素。