What's the UIKit equivalent of the prefersHomeIndicatorAutoHidden
property in SwiftUI?

- 432
- 7
- 8
-
1@Koen I did look at the doc and didn't found any information. But because the doc isn't finished yet, maybe the feature is available but not documented. – Scotow Jun 27 '19 at 17:13
-
Maybe in the next bèta. SwiftUI is far from finished. – koen Jun 27 '19 at 17:16
6 Answers
Since I could't find this in the default API either, I made it myself in a subclass of UIHostingController.
What I wanted:
var body: some View {
Text("I hide my home indicator")
.prefersHomeIndicatorAutoHidden(true)
}
Since the prefersHomeIndicatorAutoHidden
is a property on UIViewController we can override that in UIHostingController but we need to get the prefersHomeIndicatorAutoHidden
setting up the view hierarchy, from our view that we set it on to the rootView in UIHostingController.
The way that we do that in SwiftUI is PreferenceKeys. There is lots of good explanation on that online.
So what we need is a PreferenceKey to send the value up to the 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)
}
}
Now if we add .prefersHomeIndicatorAutoHidden(true)
on a View it sends the PrefersHomeIndicatorAutoHiddenPreferenceKey up the view hierarchy. To catch that in the hosting controller I made a subclass that wraps the rootView to listen to the preference change, then update the 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
}
}
Full example that doesn't expose the PreferenceKey type and has preferredScreenEdgesDeferringSystemGestures
too on git: https://gist.github.com/Amzd/01e1f69ecbc4c82c8586dcd292b1d30d

- 3,419
- 2
- 25
- 49
-
How do you call this code? I added the GitHub code and then .prefersHomeIndicatorAutoHidden(true) to the Text field of a vanilla SwiftUI project but I cannot see any effect on the home bar. – Vince O'Sullivan Feb 11 '20 at 12:40
-
1@VinceO'Sullivan Make sure you are using a PreferenceUIHostingController rather than UIHostingController. – Casper Zandbergen Feb 12 '20 at 09:22
-
-
1new SwiftUI templates don't have a scene delegate... how do we make it work for that case? – Duck Dec 12 '20 at 11:47
-
@Duck You'll have to use a UIViewControllerRepresentable to wrap everything in – Casper Zandbergen Dec 31 '20 at 19:24
For SwiftUI with the new application life cycle
From SwiftUI 2.0 when using the new Application Life Cycle we need to create a new variable in our @main .app file with the wrapper:
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
The main app file will look like this:
import SwiftUI
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Then we create our UIApplicationDelegate class in a new file:
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
}
}
Above we passed the name of our SceneDelegate class as "MySceneDelegate", so lets create this class in a separate file:
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()
}
}
}
The property prefersHomeIndicatorAutoHidden
will have to be overridden in the HostingController
class as usual as in the above solution by ShengChaLover:
class HostingController: UIHostingController<ContentView> {
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
Of course do not forget to replace contentView with the name of your view if different!
Kudos to Paul Hudson of Hacking with Swift and Kilo Loco for the hints!

- 2,898
- 2
- 22
- 29
-
Be aware that this does not *replace* standard SwiftUI 2.0 scene with default WindowGroup, but (!) creates *second* window with your custom `HostingController`. Just in case. – Asperi Oct 31 '20 at 15:32
-
Thanks for the clarification. I am still quite new myself. If you see a better way let me know. Hope SwiftUI will get a better way to handle this in the future like it handles the status bar :) – multitudes Oct 31 '20 at 15:54
-
This technique works for my initial view but when present the next one the indicator reappears. – Mark Bridges Mar 29 '21 at 10:15
-
This is my preferred answer for iOS 15. Ironically I recreated an answer that is nearly identical, but had an Info.plist entry. This eliminates the Info.plist; so I deleted mine. – Warren Stringer Oct 02 '21 at 23:30
-
It works only for my rootView, possible to make it work for other views ? – Stranger B. Nov 03 '21 at 21:54
iOS 16
you can use the .persistentSystemOverlays
and pass in .hidden
to hide all non-transient system views that are automatically placed over our UI
Text("Goodbye home indicator, the multitask indicator on iPad, and more.")
.persistentSystemOverlays(.hidden)

- 95,414
- 31
- 268
- 278
-
Note: The system decides if the overlays are hidden or not. In my case that doesn't work and I don't know why... https://developer.apple.com/documentation/swiftui/view/persistentsystemoverlays(_:) – Nico S. Sep 20 '22 at 12:04
-
Even checking `requires fullscreen` in the project file doesn't help. Any ideas? – Nico S. Sep 29 '22 at 01:02
The only solution i found to work 100% of the time was swizzling the instance property 'prefersHomeIndicatorAutoHidden' in all UIViewControllers that way it always returned true.
Create a extension on NSObject for swizzling instance methods / properties
//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!)
}
}
Created extension on UIViewController this will swap the instance property in all view controller with one we created that always returns 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)
}
}
Then call swizzleHomeIndicatorProperty() function in your App Delegate
// 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
}
}
if using SwiftUI register your AppDelegate using UIApplicationDelegateAdaptor
//Application.swift
@main
struct Application: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

- 1,137
- 1
- 11
- 21
I have managed to hide the Home Indicator in my single view app using a technique that's simpler than what Casper Zandbergen proposes. It's way less 'generic' and I am not sure the preference will propagate down the view hierarchy, but in my case that's just enough.
In your SceneDelegate subclass the UIHostingController with your root view type as the generic parameter and override prefersHomeIndicatorAutoHidden property.
class HostingController: UIHostingController<YourRootView> {
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
}
In the scene method's routine create an instance of you custom HostingController passing the root view as usual and assign that instance to window's 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()
}
Update: this will not work if you need to inject an EnvironmentObject into a root view.

- 614
- 5
- 10
-
This turns off the home indicator everywhere in your app, I wouldn't recommend doing that. – Casper Zandbergen Dec 11 '19 at 08:24
-
@CasperZandbergen considering the indicator is not customizable, I cannot agree that it is bad idea to hide it everywhere. The glaring white indicator looks absolutely terrible on black backgrounds. Also, this solution does not fully remove it as one may think but rather dims after a period of inactivity. – Shengchalover Dec 12 '19 at 18:32
-
Hiding it disables the close app swipe and will annoy users. You can actually change the color of it by placing a view behind it. – Casper Zandbergen Dec 14 '19 at 18:22
-
@CasperZandbergen hiding it *does not* disable swipe, the indicator reappears as soon as you begin the swipe gesture and everything works smoothly. At least in the above implementation. What annoys users is highly subjective. Personally, I am annoyed with that indicator in dark mode and I am glad I can hide it. As for the trick regarding color, will check it out. Thanks for the tip. – Shengchalover Dec 15 '19 at 21:03
My solution is made for one screen only (UIHostingController
). It means you do not need to replace UIHostingController
in the whole app and deal with AppDelegate
. Thus it will not affect injection of your EnvironmentObject
s into ContentView
. If you want to have just one presented screen with hideable home indicator, you need to wrap your view around custom UIHostingController
and present it.
This can be done so (or you can also use PreferenceUIHostingController
like in previous answers if you want to change the property in runtime. But I guess it will require some more workarounds):
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
}
}
Then you have to present your HomeIndicatorHideableHostingController
in
UIKit style (tested on iOS 14). The solution is based on this: https://gist.github.com/fullc0de/3d68b6b871f20630b981c7b4d51c8373. If you want to adapt it to iOS 13 look through the link (topMost
property is also found there).
You create view modifier for it just like 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))
}
}
Modifer itself:
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)
}
}
}
}
}
And then you use it like this:
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")
}
}
}

- 41
- 4