在许多情况下,我们希望某一个视图能够锁定在一个特定的方向下,例如:相机页面等
本文将分享一种我自己捣腾出来的一种实现方式以及背后的逻辑。
本文仅适用于 iOS
实现逻辑
思路是使用 @UIApplicationDelegateAdaptor(AppDelegate.self)
配合 AppDelegate 中的 func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
来实现方向的锁定。
在需要锁定方向的 View 中,修改 AppDelegate 中的 orientationLock
并重新返回一个全局的 UIInterfaceOrientationMask
。
退出锁定的 View 之后,只需要还原 orientationLock
并重新返回 UIInterfaceOrientationMask
即可。
踩过的坑
@UIApplicationDelegateAdaptor
需要创建在程序入口处,即 @main
对应的 App Protocol 下,否则,AppDelegate 不起作用。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import SwiftUI
@main struct TestApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene { WindowGroup { ContentView() } } }
|
在 iOS 15 及以下和 iOS 16+ 的旋转策略是不同的,即系统调用 func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
的时机和需要做的事情是不同的。
iOS 15 及以下:在每次转动屏幕的时候都会调用该方法重新评估以决定是否完成 UI 旋转,如果当前的设备方向不在 UIInterfaceOrientationMask 中,也不会自动将屏幕旋转至正确的方向。
因此,除了要设定好 supportedInterfaceOrientations
之外,还需要判断下当前的屏幕方向,根据具体情况旋转到正确的方向上。
iOS 16+:在屏幕旋转的时候并不会再调用 func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
了,因此,旋转设备时系统不会一直刷新 UIInterfaceOrientationMask
。
在 iOS 16 屏幕旋转适配 找到了解决方案:通过在 rootViewController 中调用 setNeedsUpdateOfSupportedInterfaceOrientations()
即可重新调用该方法更新 UIInterfaceOrientationMask
。
实测发现,若当前的设备方向不在 UIInterfaceOrientationMask
中,会自动调整。
对于 iOS 15 及以下还有一个 bug,如果设备方向没有发生改变而方向锁发生了改变(例如:设备是 .portrait,方向锁从 .landscapeLeft 变到 .portrait 时),则此时无法自动旋转到正确的方向。
原因是:虽然程序化执行了方向更新,但是其前后结果一致(设备方向实际不发生变化),系统就不会重新获取 UIInterfaceOrientationMask
,即保持了原来的 Mask 没有更新。
解决方案是:在最后使用 NotificationCenter.default.post(name: UIDevice.orientationDidChangeNotification, object: nil)
手动发送屏幕旋转的通知,让系统重新获取 UIInterfaceOrientationMask
。
API 封装
完成了可行性验证之后决定做成一个可以在任何视图下使用的 View Modifier,其中需要实现以下功能:
- 单个页面的方向锁
- 能够设定默认方向遮罩,用于恢复 / 解锁某个页面设定的方向锁
- 由数据驱动,支持程序化改变该页面的方向
因为要实现数据驱动,因此不能只是简单的使用 onAppear
和 onDisappear
,当数据变动的时候也需要响应。
这里我选择使用 Preference
+ onPreferenceChange
来实现(这里并不是只能用 Preference,也可以使用 onChange
和 task(id:)
)
我们将 AppDelegate
和 View + Extension
合并在一起,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
| import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate { static var defaultOrientation = UIInterfaceOrientationMask.allButUpsideDown static var orientationLock = UIInterfaceOrientationMask.allButUpsideDown func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { AppDelegate.orientationLock } }
struct DefaultOrientationMask: PreferenceKey { static var defaultValue: UIInterfaceOrientationMask { UIDevice.current.userInterfaceIdiom == .pad ? .all : .allButUpsideDown } static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) { value = nextValue() } }
struct DeviceOrientationMask: PreferenceKey { static var defaultValue: UIInterfaceOrientationMask { UIDevice.current.userInterfaceIdiom == .pad ? .all : .allButUpsideDown } static func reduce(value: inout UIInterfaceOrientationMask, nextValue: () -> UIInterfaceOrientationMask) { value = nextValue() } }
extension View { private func updateOrientation(_ orientation: UIInterfaceOrientationMask) { UIInterfaceOrientation.updateOrientation(orientation) } func deviceOrientation(_ orientation: UIInterfaceOrientationMask) -> some View { preference( key: DeviceOrientationMask.self, value: orientation ) .onPreferenceChange(DeviceOrientationMask.self, perform: updateOrientation(_:)) .onAppear { updateOrientation(orientation) } .onDisappear { updateOrientation(AppDelegate.defaultOrientation) } } func defaultOrientationMask(_ orientation: UIInterfaceOrientationMask) -> some View { preference( key: DefaultOrientationMask.self, value: orientation ) .onPreferenceChange(DefaultOrientationMask.self) { AppDelegate.defaultOrientation = $0 guard AppDelegate.defaultOrientation == AppDelegate.orientationLock else { return } updateOrientation($0) } .onAppear { updateOrientation(orientation) } } }
extension UIInterfaceOrientation { init?(deviceOrientation: UIDeviceOrientation) { let orientation: UIInterfaceOrientation = { switch deviceOrientation { case .portraitUpsideDown: UIInterfaceOrientation.portraitUpsideDown case .landscapeLeft: UIInterfaceOrientation.landscapeLeft case .landscapeRight: UIInterfaceOrientation.landscapeRight default: UIInterfaceOrientation.portrait } }() self.init(rawValue: orientation.rawValue) } static func updateOrientation(_ orientation: UIInterfaceOrientationMask) { AppDelegate.orientationLock = orientation if #available(iOS 16.0, *) { let scenes = UIApplication.shared.connectedScenes let windowScene = scenes.first as? UIWindowScene let window = windowScene?.windows.first window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() } else { let newOrientation: UIInterfaceOrientation = { switch orientation { case .portrait: return .portrait case .landscapeLeft: return .landscapeLeft case .landscapeRight: return .landscapeRight case .portraitUpsideDown: return .portraitUpsideDown default: let current = UIInterfaceOrientation(deviceOrientation: UIDevice.current.orientation)! guard orientation != .all else { return current } if orientation == .allButUpsideDown { return current == .portraitUpsideDown ? .portrait : current } if current != .landscapeLeft && current != .landscapeRight { return .landscapeLeft } else { return current } } }() UIDevice.current.setValue(newOrientation.rawValue, forKey: "orientation") NotificationCenter.default.post(name: UIDevice.orientationDidChangeNotification, object: nil) } } }
|