【问题标题】:How to hide the home indicator with SwiftUI?如何使用 SwiftUI 隐藏主页指示器?
【发布时间】:2019-06-27 16:49:55
【问题描述】:

SwiftUI 中 prefersHomeIndicatorAutoHidden 属性的 UIKit 等价物是什么?

【问题讨论】:

  • @Koen 我确实查看了文档,但没有找到任何信息。但由于文档尚未完成,可能该功能可用但未记录。
  • 也许在下一个测试版中。 SwiftUI 远未完成。

标签: ios swift swiftui


【解决方案1】:

因为我在默认 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

【讨论】:

  • 这段代码怎么称呼?我添加了 GitHub 代码,然后将 .prefersHomeIndicatorAutoHidden(true) 添加到 vanilla SwiftUI 项目的 Text 字段,但我看不到对主页栏有任何影响。
  • @VinceO'Sullivan 确保您使用的是 PreferenceUIHostingController 而不是 UIHostingController。
  • 啊,在 SceneDelegate 中。错过了那一步。谢谢。
  • 新的 SwiftUI 模板没有场景委托......我们如何使它适用于这种情况?
  • @Duck 你必须使用 UIViewControllerRepresentable 来包装所有内容
【解决方案2】:

对于具有新应用程序生命周期的 SwiftUI

从 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 的提示!

【讨论】:

  • 请注意,这不会用默认的 WindowGroup 替换标准 SwiftUI 2.0 场景,而是 (!) 使用您的自定义 HostingController 创建 second 窗口.以防万一。
  • 感谢您的澄清。我自己还是很新的。如果您看到更好的方法,请告诉我。希望 SwiftUI 将来能像处理状态栏一样获得更好的处理方式:)
  • 这种技术适用于我的初始视图,但是当呈现下一个时,指示器会重新出现。
  • 这是我对 iOS 15 的首选答案。具有讽刺意味的是,我重新创建了一个几乎相同的答案,但有一个 Info.plist 条目。这消除了 Info.plist;所以我删除了我的。
  • 它只适用于我的rootView,是否可以使它适用于其他视图?
【解决方案3】:

我使用一种比 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 注入根视图,此将不起作用

【讨论】:

  • 这会关闭您应用中各处的主页指示器,我不建议这样做。
  • @CasperZandbergen 考虑到该指标不可定制,我不同意到处隐藏它是个坏主意。耀眼的白色指示器在黑色背景上看起来非常糟糕。此外,此解决方案并没有像人们想象的那样完全删除它,而是在一段时间不活动后变暗。
  • 隐藏它会禁用关闭应用滑动并会惹恼用户。实际上,您可以通过在其后面放置一个视图来更改它的颜色。
  • @CasperZandbergen 隐藏它不会禁用滑动,一旦您开始滑动手势,指示器就会重新出现,并且一切正常。至少在上述实现中。惹恼用户的是高度主观的。就个人而言,我对黑暗模式下的那个指示器感到恼火,我很高兴我可以隐藏它。至于关于颜色的技巧,将检查它。感谢您的提示。
【解决方案4】:

我的解决方案仅适用于一个屏幕 (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")
        }
    }
}

【讨论】:

    【解决方案5】:

    我发现唯一可以 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()
          }
       }
    }
    

    【讨论】:

      猜你喜欢
      • 2022-06-20
      • 1970-01-01
      • 2018-03-23
      • 2018-09-18
      • 2014-06-24
      • 2019-10-22
      • 1970-01-01
      • 1970-01-01
      • 2015-09-23
      相关资源
      最近更新 更多