如何使用SwiftUI隐藏主页指示器?

时间:2019-06-27 16:49:55

标签: ios swift swiftui

SwiftUI中prefersHomeIndicatorAutoHidden属性的UIKit等效于什么?

4 个答案:

答案 0 :(得分:2)

我设法使用比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()
}

答案 1 :(得分:2)

我的解决方案仅适用于一个屏幕 (UIHostingController)。这意味着您不需要在整个应用程序中替换 UIHostingController 并处理 AppDelegate。因此,它不会影响将您的 EnvironmentObject 注入 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,请查看链接(此处还可以找到 HomeIndicatorHideableHostingController 属性)。

您可以像 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)
                    }
                }
            }
    }
}

答案 2 :(得分:1)

由于我也无法在默认API中找到它,所以我自己在UIHostingController的子类中创建了它。

我想要的是:

var body: some View {
    Text("I hide my home indicator")
        .prefersHomeIndicatorAutoHidden(true)
}

由于prefersHomeIndicatorAutoHidden是UIViewController的属性,因此我们可以在UIHostingController中覆盖它,但是我们需要从视图中将prefersHomeIndicatorAutoHidden设置为rootView,将其设置为rootView UIHostingController。

在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

答案 3 :(得分:0)

SwiftUI 2.0通用解决方案

在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类中的属性HostingControllerclass HostingController: UIHostingController<ContentView> { override var prefersHomeIndicatorAutoHidden: Bool { return true } } 类中进行覆盖:

const Generator = require('yeoman-generator');

module.exports = class extends Generator {
    // The name `constructor` is important here
    constructor(args, opts) {
        // Calling the super constructor is important so our generator is correctly set up
        super(args, opts);

        // Next, add your custom code
        this.argument("appname", {type: String, required: false});
    }

    async prompting(){
        const answers = await this.prompt([
            {
                type: "input",
                name: "name",
                message: "Your project name",
                default: this.appname
            },
            {
                type: "confirm",
                name: "cool",
                message: "Would you like to enable the Cool Feature?"
            },
            {
                type: "list",
                name: "features",
                message: "What do you want to make today?",
                choices: [
                    "Some tests",
                    "Other tests, why not!",
                    "Another test?",
                    "Yes."
                ]
            },
            {
                type: "checkbox",
                name: "more",
                message: "Anything more?",
                choices: [
                    {
                        name: "I'm a bit bored",
                        value: "bored",
                        checked: true
                    },
                    {
                        name: "I'm busy doing something else.",
                        value: "smthelse",
                        checked: false
                    }
                ]
            }
        ]);
        this.log("app name", answers.name);
        this.log("cool feature", answers.cool);
        this.log("feature", answers.features);
        this.log("more", answers.more.join(" + "))
    }
};

当然不要忘了用不同的视图名称替换contentView!

对Swift和Kilo Loco的黑客入侵的Paul Hudson表示感谢!