【发布时间】:2019-06-27 16:49:55
【问题描述】:
SwiftUI 中 prefersHomeIndicatorAutoHidden 属性的 UIKit 等价物是什么?
【问题讨论】:
-
@Koen 我确实查看了文档,但没有找到任何信息。但由于文档尚未完成,可能该功能可用但未记录。
-
也许在下一个测试版中。 SwiftUI 远未完成。
SwiftUI 中 prefersHomeIndicatorAutoHidden 属性的 UIKit 等价物是什么?
【问题讨论】:
因为我在默认 API 中也找不到它,所以我自己在 UIHostingController 的子类中创建了它。
我想要的:
var body: some View {
Text("I hide my home indicator")
.prefersHomeIndicatorAutoHidden(true)
}
由于 prefersHomeIndicatorAutoHidden 是 UIViewController 上的一个属性,我们可以在 UIHostingController 中覆盖它,但我们需要从我们将其设置到 UIHostingController 中的 rootView 的视图中获取 prefersHomeIndicatorAutoHidden 设置视图层次结构。
我们在 SwiftUI 中这样做的方式是 PreferenceKeys。网上有很多很好的解释。
所以我们需要一个 PreferenceKey 来将值发送到 UIHostingController:
struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey {
typealias Value = Bool
static var defaultValue: Value = false
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue() || value
}
}
extension View {
// Controls the application's preferred home indicator auto-hiding when this view is shown.
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View {
preference(key: PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value)
}
}
现在,如果我们在视图上添加.prefersHomeIndicatorAutoHidden(true),它会将 PrefersHomeIndicatorAutoHiddenPreferenceKey 向上发送到视图层次结构。为了在宿主控制器中捕捉到这一点,我创建了一个包含 rootView 的子类来监听偏好变化,然后更新UIViewController.prefersHomeIndicatorAutoHidden:
// Not sure if it's bad that I cast to AnyView but I don't know how to do this with generics
class PreferenceUIHostingController: UIHostingController<AnyView> {
init<V: View>(wrappedView: V) {
let box = Box()
super.init(rootView: AnyView(wrappedView
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) {
box.value?._prefersHomeIndicatorAutoHidden = $0
}
))
box.value = self
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private class Box {
weak var value: PreferenceUIHostingController?
init() {}
}
// MARK: Prefers Home Indicator Auto Hidden
private var _prefersHomeIndicatorAutoHidden = false {
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
}
override var prefersHomeIndicatorAutoHidden: Bool {
_prefersHomeIndicatorAutoHidden
}
}
不公开 PreferenceKey 类型并且在 git 上也有 preferredScreenEdgesDeferringSystemGestures 的完整示例:https://gist.github.com/Amzd/01e1f69ecbc4c82c8586dcd292b1d30d
【讨论】:
从 SwiftUI 2.0 开始,当使用新的应用程序生命周期时,我们需要使用包装器在 @main .app 文件中创建一个新变量:
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
主应用文件如下所示:
import SwiftUI
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
然后我们在一个新文件中创建我们的 UIApplicationDelegate 类:
import UIKit
class MyAppDelegate: NSObject, UIApplicationDelegate {
func application(
_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let config = UISceneConfiguration(name: "My Scene Delegate", sessionRole: connectingSceneSession.role)
config.delegateClass = MySceneDelegate.self
return config
}
}
上面我们将 SceneDelegate 类的名称传递为“MySceneDelegate”,所以让我们在单独的文件中创建这个类:
class MySceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let rootView = ContentView()
let hostingController = HostingController(rootView: rootView)
window.rootViewController = hostingController
self.window = window
window.makeKeyAndVisible()
}
}
}
属性prefersHomeIndicatorAutoHidden 必须像上面的ShengChaLover 解决方案一样在HostingController 类中被覆盖:
class HostingController: UIHostingController<ContentView> {
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
当然不要忘记将 contentView 替换为你的视图的名称,如果不同的话!
感谢 Paul Hudson 的 Hacking with Swift 和 Kilo Loco 的提示!
【讨论】:
HostingController 创建 second 窗口.以防万一。
我使用一种比 Casper Zandbergen 提出的更简单的技术设法在我的单视图应用程序中隐藏 Home Indicator。它不那么“通用”,我不确定偏好是否会在视图层次结构中传播,但就我而言,这已经足够了。
在您的 SceneDelegate 子类中,使用您的根视图类型作为通用参数的 UIHostingController 并覆盖 prefersHomeIndicatorAutoHidden 属性。
class HostingController: UIHostingController<YourRootView> {
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
在场景方法的例程中创建一个自定义 HostingController 的实例,像往常一样传递根视图并将该实例分配给窗口的 rootViewController:
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let rootView = YourRootView()
let hostingController = HostingController(rootView: rootView)
window.rootViewController = hostingController
self.window = window
window.makeKeyAndVisible()
}
更新:如果您需要将 EnvironmentObject 注入根视图,此将不起作用。
【讨论】:
我的解决方案仅适用于一个屏幕 (UIHostingController)。这意味着您不需要在整个应用程序中替换UIHostingController 并处理AppDelegate。因此它不会影响将您的EnvironmentObjects 注入ContentView。如果您只想显示一个带有可隐藏主页指示器的屏幕,则需要将您的视图包裹在自定义 UIHostingController 周围并显示它。
可以这样做(或者如果您想在运行时更改属性,也可以像以前的答案一样使用PreferenceUIHostingController。但我想这需要更多的解决方法):
final class HomeIndicatorHideableHostingController: UIHostingController<AnyView> {
init<V: View>(wrappedView: V) {
super.init(rootView: AnyView(wrappedView))
}
@objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
然后你必须在
UIKit 样式(在 iOS 14 上测试)。解决方案基于此:https://gist.github.com/fullc0de/3d68b6b871f20630b981c7b4d51c8373。如果你想让它适应 iOS 13,请查看链接(topMost 属性也在那里)。
你为它创建视图修饰符就像fullScreenCover:
public extension View {
/// This is used for presenting any SwiftUI view in UIKit way.
///
/// As it uses some tricky way to make the objective,
/// could possibly happen some issues at every upgrade of iOS version.
/// This way of presentation allows to present view in a custom `UIHostingController`
func uiKitFullPresent<V: View>(isPresented: Binding<Bool>,
animated: Bool = true,
transitionStyle: UIModalTransitionStyle = .coverVertical,
presentStyle: UIModalPresentationStyle = .fullScreen,
content: @escaping (_ dismissHandler:
@escaping (_ completion:
@escaping () -> Void) -> Void) -> V) -> some View {
modifier(FullScreenPresent(isPresented: isPresented,
animated: animated,
transitionStyle: transitionStyle,
presentStyle: presentStyle,
contentView: content))
}
}
修改器本身:
public struct FullScreenPresent<V: View>: ViewModifier {
typealias ContentViewBlock = (_ dismissHandler: @escaping (_ completion: @escaping () -> Void) -> Void) -> V
@Binding var isPresented: Bool
let animated: Bool
var transitionStyle: UIModalTransitionStyle = .coverVertical
var presentStyle: UIModalPresentationStyle = .fullScreen
let contentView: ContentViewBlock
private weak var transitioningDelegate: UIViewControllerTransitioningDelegate?
init(isPresented: Binding<Bool>,
animated: Bool,
transitionStyle: UIModalTransitionStyle,
presentStyle: UIModalPresentationStyle,
contentView: @escaping ContentViewBlock) {
_isPresented = isPresented
self.animated = animated
self.transitionStyle = transitionStyle
self.presentStyle = presentStyle
self.contentView = contentView
}
@ViewBuilder
public func body(content: Content) -> some View {
content
.onChange(of: isPresented) { _ in
if isPresented {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
let topMost = UIViewController.topMost
let rootView = contentView { [weak topMost] completion in
topMost?.dismiss(animated: animated) {
completion()
isPresented = false
}
}
let hostingVC = HomeIndicatorHideableHostingController(wrappedView: rootView)
if let customTransitioning = transitioningDelegate {
hostingVC.modalPresentationStyle = .custom
hostingVC.transitioningDelegate = customTransitioning
} else {
hostingVC.modalPresentationStyle = presentStyle
if presentStyle == .overFullScreen {
hostingVC.view.backgroundColor = .clear
}
hostingVC.modalTransitionStyle = transitionStyle
}
topMost?.present(hostingVC, animated: animated, completion: nil)
}
}
}
}
}
然后你像这样使用它:
struct ContentView: View {
@State var modalPresented: Bool = false
var body: some View {
Button(action: {
modalPresented = true
}) {
Text("First view")
}
.uiKitFullPresent(isPresented: $modalPresented) { closeHandler in
SomeModalView(close: closeHandler)
}
}
}
struct SomeModalView: View {
var close: (@escaping () -> Void) -> Void
var body: some View {
Button(action: {
close({
// Do something when dismiss animation finished
})
}) {
Text("Tap to go back")
}
}
}
【讨论】:
我发现唯一可以 100% 工作的解决方案是在所有 UIViewController 中调整实例属性“prefersHomeIndicatorAutoHidden”,这样它总是返回 true。
在 NSObject 上创建一个扩展,用于调配实例方法/属性
//NSObject+Swizzle.swift
extension NSObject {
class func swizzle(origSelector: Selector, withSelector: Selector, forClass: AnyClass) {
let originalMethod = class_getInstanceMethod(forClass, origSelector)
let swizzledMethod = class_getInstanceMethod(forClass, withSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!)
}
}
在 UIViewController 上创建扩展,这会将所有视图控制器中的实例属性与我们创建的始终返回 true 的实例属性交换
//UIViewController+HideHomeIndicator.swift
extension UIViewController {
@objc var swizzle_prefersHomeIndicatorAutoHidden: Bool {
return true
}
public class func swizzleHomeIndicatorProperty() {
self.swizzle(origSelector:#selector(getter: UIViewController.prefersHomeIndicatorAutoHidden),
withSelector:#selector(getter: UIViewController.swizzle_prefersHomeIndicatorAutoHidden),
forClass:UIViewController.self)
}
}
然后在您的 App Delegate 中调用 swizzleHomeIndicatorProperty() 函数
// AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//Override 'prefersHomeIndicatorAutoHidden' in all UIViewControllers
UIViewController.swizzleHomeIndicatorProperty()
return true
}
}
如果使用 SwiftUI,则使用 UIApplicationDelegateAdaptor 注册您的 AppDelegate
//Application.swift
@main
struct Application: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
【讨论】: