I already spent a lot of hours trying to figure out a way to change statusBarStyle to light/dark using the new lifecycle SwiftUI App.
The newest posts about the status bar teach how to hide it, but I don't want to do it, I just need to change it to dark or light.
To change the color, the most recent way I found is open SceneDelegate.swift and change window.rootViewController to use my own HostingController, but it will only work for projects using UIKit App Delegate Lifecycle. Using SwiftUI App Lifecycle, the SceneDelegate.swift will not be generated, so where can I do it?
I can do it via General Settings on the Xcode interface. My question is about how to do it via code dynamically.
Target: iOS 14
IDE: Xcode 12 beta 3
OS: MacOS 11 Big Sur
Below is what I got so far.
Everything.swift
import Foundation
import SwiftUI
class LocalStatusBarStyle { // style proxy to be stored in Environment
fileprivate var getter: () -> UIStatusBarStyle = { .default }
fileprivate var setter: (UIStatusBarStyle) -> Void = {_ in}
var currentStyle: UIStatusBarStyle {
get { self.getter() }
set { self.setter(newValue) }
}
}
struct LocalStatusBarStyleKey: EnvironmentKey {
static let defaultValue: LocalStatusBarStyle = LocalStatusBarStyle()
}
extension EnvironmentValues { // Environment key path variable
var localStatusBarStyle: LocalStatusBarStyle {
get {
return self[LocalStatusBarStyleKey.self]
}
}
}
class MyHostingController<Content>: UIHostingController<Content> where Content:View {
private var internalStyle = UIStatusBarStyle.default
#objc override dynamic open var preferredStatusBarStyle: UIStatusBarStyle {
get {
internalStyle
}
set {
internalStyle = newValue
self.setNeedsStatusBarAppearanceUpdate()
}
}
override init(rootView: Content) {
super.init(rootView:rootView)
LocalStatusBarStyleKey.defaultValue.getter = { self.preferredStatusBarStyle }
LocalStatusBarStyleKey.defaultValue.setter = { self.preferredStatusBarStyle = $0 }
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
struct TitlePage: View {
#Environment(\.localStatusBarStyle) var statusBarStyle
#State var title: String
var body: some View {
Text(title).onTapGesture {
if self.statusBarStyle.currentStyle == .darkContent {
self.statusBarStyle.currentStyle = .default
self.title = "isDefault"
} else {
self.statusBarStyle.currentStyle = .darkContent
self.title = "isDark"
}
}
}
}
struct ContainerView: View {
var controllers: [MyHostingController<TitlePage>]
init(_ titles: [String]) {
self.controllers = titles.map { MyHostingController(rootView: TitlePage(title: $0)) }
}
var body: some View {
PageViewController(controllers: self.controllers)
}
}
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
return pageViewController
}
func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) {
uiViewController.setViewControllers([controllers[0]], direction: .forward, animated: true)
}
typealias UIViewControllerType = UIPageViewController
}
MyApp.swift
import SwiftUI
#main
struct TestAppApp: App {
var body: some Scene {
WindowGroup {
ContainerView(["Subscribe", "Comment"])
}
}
}
struct TestAppApp_Previews: PreviewProvider {
static var previews: some View {
Text("Hello, World!")
}
}
Add two values to the Info.plist:
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string>
This works with the new SwiftUI App Lifecycle (#main). Verified on iOS14.4.
My suggestion is you just create an AppDelegate Adaptor and do whatever customization you need from there. SwiftUI will handle the creation of AppDelegate and managing its lifetime.
Create an AppDelegate class:
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UIApplication.shared.statusBarStyle = .darkContent
return true
}
}
Now inside your App:
#main
struct myNewApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
Text("I am a New View")
}
}
}
This is not really a solution but the best I could come up with (and ended up doing) was to force app to the dark mode. Either in Info.plist or NavigationView { ... }.preferredColorScheme(.dark)
That will also change the statusBar. You will not be able to change the status bar style per View though.
Before, with SceneDelegate
(code taken from SwiftUI: Set Status Bar Color For a Specific View)
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
...
window.rootViewController = MyHostingController(rootView: contentView)
}
After, with no SceneDelegate
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MyHostingController(rootView: ContentView())
}
}
}
If you follow the answer from the link above and apply it here with the #main, you should be able to achieve your changes.
Related
Goal
Trying to set up my app so that some screens are forced into landscape mode and some portrait mode. When those screens become visible, (after appearance / re-appearance), the screen orients to the proper orientation.
Problem
In the below example, the screen auto-orients to the first screen correctly, and when the next sheet is presented, correctly updates. However, after dismissing the sheets, the app fails to re-orient the screen back to landscape mode. Giving the error:
Error Domain=UISceneErrorDomain Code=101 "None of the requested orientations are supported by the view controller. Requested: landscapeLeft, landscapeRight; Supported: portrait" UserInfo={NSLocalizedDescription=None of the requested orientations are supported by the view controller. Requested: landscapeLeft, landscapeRight; Supported: portrait}
This is unexpected, because the View.setOrientation() method should set the supported orientations to .all before attempting to re-orient the screen.
Code
import SwiftUI
#main
struct TestApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup("WindowGroup") {
VStack() {
LandscapeView()
}
}
}
}
class AppDelegate: NSObject, UIApplicationDelegate,ObservableObject {
static var orientationLock = UIInterfaceOrientationMask.all
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
print("Application supportedInterfaceOrientations Ran providing: \(AppDelegate.orientationLock)")
return AppDelegate.orientationLock
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
if connectingSceneSession.role == .windowApplication {
configuration.delegateClass = SceneDelegate.self
}
return configuration
}
}
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow? // << contract of `UIWindowSceneDelegate`
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = windowScene.keyWindow // << store !!!
}
}
struct LandscapeView: View {
#EnvironmentObject var sceneDelegate: SceneDelegate
#State private var portraitSheetPresented = false
#State var isVisible:Bool = false
var body: some View {
HStack() {
Button("Forced Landscape") {
portraitSheetPresented = true
}
.sheet(isPresented: $portraitSheetPresented) {
PortraitModal(parentViewVisible: $isVisible)
}
}
.frame(maxWidth:.infinity,maxHeight:.infinity)
.onAppear() {
isVisible = true
}
.onChange(of: isVisible) { isVisible in
if isVisible {
print("is visibile again")
setOrientation(sceneDelegate, .landscape)
}
}
}
}
struct PortraitModal: View {
#Environment(\.dismiss) private var dismiss
#Binding var parentViewVisible: Bool
#State var isVisible:Bool = false
#EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
VStack() {
Text("Forced Portrait")
.onTapGesture {
dismiss()
}
}
.onAppear() {
$parentViewVisible.wrappedValue = false
isVisible = true
}
.onChange(of: isVisible) { isVisible in
if isVisible {
setOrientation(sceneDelegate, .portrait)
}
}
.onDisappear() {
$parentViewVisible.wrappedValue = true
}
.frame(maxWidth:.infinity,maxHeight:.infinity)
}
}
extension View {
func setOrientation(_ sceneDelegate:SceneDelegate, _ orientation:UIInterfaceOrientationMask) {
// set supported orientations to the new orientation
AppDelegate.orientationLock = orientation
// get Window Scene
guard let windowScene = sceneDelegate.window?.windowScene else {return}
//force view into orientation
if #available(iOS 16.0, *) {
windowScene.requestGeometryUpdate(UIWindowScene.GeometryPreferences.iOS(interfaceOrientations: orientation)) { error in
print(error)
}
} else {
// Fallback to some earlier versions
}
}
}
I'm trying to make a simple swiftui app using qualtrics and I'm trying to use a uiviewrepresentable to make it work
#main
struct QualtricsPocApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
init() {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// i have the actual intercept id's here i just removed them
Qualtrics.shared.initializeProject(brandId: "brand", projectId: "proj", extRefId: "ref", completion: { (myInitializationResult) in print(myInitializationResult);})
return true
}
}
}
struct QualtricsViewRep: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
Qualtrics.shared.evaluateProject { (targetingResults) in
for (interceptID, result) in targetingResults {
if result.passed() {
let displayed = Qualtrics.shared.display(viewController: self, autoCloseSurvey: true)
}
}
}
}
on let displayed = ... I keep getting the error "Cannot convert value of type 'QualtricsViewRep' to expected argument type 'UIViewController'", how can I return this code as a UIViewController to use in a swiftui app, or is there some other way I should be approaching this?
Unfortunately I don't have Qualtrics installed, but I have worked with it within UIKit. My assumption here is that you will need to create an instance of a UIViewController. This view controller is what the qualtrics view will present itself over.
Ultimately, you will return the view controller which contains the qualtrics view presented over it.
struct QualtricsViewRep: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
Qualtrics.shared.evaluateProject { (targetingResults) in
for (interceptID, result) in targetingResults {
if result.passed() {
DispatchQueue.main.async {
let vc = UIViewController()
let displayed = Qualtrics.shared.display(viewController: vc, autoCloseSurvey: true)
}
}
}
}
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
// your code here
}
}
Sample Qualtrics SwiftUI App
I had to implement this for work so I decided to share my solution.
QualtricsDemoApp
import SwiftUI
import Qualtrics
#main
struct QualtricsDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.task {
Qualtrics.shared.initializeProject(brandId: "BRAND_ID", projectId: "PROJECT_ID", extRefId: "EXT_REF_ID") { myInitializationResult in
print(myInitializationResult)
}
}
}
}
}
ContentView
import SwiftUI
import Qualtrics
struct ContentView: View {
#State private var showFeedback = false
var body: some View {
VStack {
Button("Show Qualtrics Feedback") {
showFeedback.toggle()
}
if showFeedback {
QualtricsFeedbackRepresentable()
}
}
.font(.title)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct QualtricsFeedbackRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let vc = UIViewController()
Qualtrics.shared.evaluateProject { (targetingResults) in
for (_, result) in targetingResults {
if result.passed() {
Qualtrics.shared.display(viewController: vc)
}
}
}
return vc
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
Now I just have to figure out why it's not in English. 😃
I am trying to change the appearance of MFMailComposeViewController's navbar. The UINavigationBar appearance is set globally (inside AppDelegate's didFinishLaunchingWithOptions function) to the desired colours, however, when the MFMailComposeViewController opens, the navbar changes back to default with a little delay.
What I've tried (based on some previous answers for these kinds of questions):
setting the viewController's navbar in the makeUIViewController function
calling the appearance methods (UINavigationBar.appearance()) before the initialization of the MFMailComposeViewController
override the MFMailComposeViewController's viewDidLoad method
Is this a bug, and if not, how could I change the appearance of the MFMailComposeViewController's navbar?
How to reproduce: (Xcode version: 12.3, iOS version: 14.3)
MailViewNavbarApp.swift
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
let coloredNavAppearance = UINavigationBarAppearance()
coloredNavAppearance.configureWithOpaqueBackground()
coloredNavAppearance.backgroundColor = .gray
coloredNavAppearance.titleTextAttributes = [.foregroundColor: UIColor.yellow]
coloredNavAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.yellow]
UINavigationBar.appearance().tintColor = .yellow
UINavigationBar.appearance().standardAppearance = coloredNavAppearance
UINavigationBar.appearance().scrollEdgeAppearance = coloredNavAppearance
return true
}
}
#main
struct MailViewNavbarApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
MailView.swift
struct MailView: UIViewControllerRepresentable {
#Binding var isShowing: Bool
let email: String
func makeCoordinator() -> Coordinator {
Coordinator(isShowing: $isShowing)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MailView>) -> MFMailComposeViewController {
let vc = MFMailComposeViewController()
vc.setToRecipients([email])
vc.mailComposeDelegate = context.coordinator
return vc
}
func updateUIViewController(
_ vc: MFMailComposeViewController,
context _: UIViewControllerRepresentableContext<MailView>
) {}
class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
#Binding var isShowing: Bool
init(isShowing: Binding<Bool>) {
_isShowing = isShowing
}
func mailComposeController(_: MFMailComposeViewController,
didFinishWith _: MFMailComposeResult,
error: Error?) {
isShowing = false
}
}
}
ContentView.swift
import SwiftUI
import MessageUI
struct ContentView: View {
#State var isShowingMailView = false
var body: some View {
Button(action: {
self.isShowingMailView.toggle()
}) {
Text("Tap me")
}
.disabled(!MFMailComposeViewController.canSendMail())
.sheet(isPresented: $isShowingMailView) {
MailView(
isShowing: $isShowingMailView,
email: "test#email.com"
)
}
}
}
Thanks for your help!
How can I hide the Title Bar in the new SwiftUI App Protocol?
Since the AppDelegate.swift and SceneDelegate.swift protocols are gone, I cant follow this documentation anymore:
https://developer.apple.com/documentation/uikit/mac_catalyst/removing_the_title_bar_in_your_mac_app_built_with_mac_catalyst
I can't implement this code:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
#if targetEnvironment(macCatalyst)
if let titlebar = windowScene.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
Hope it's still possible with the new AppProtocol..
Thanks in advance
This is how to hide the titlebar:
struct ContentView: View {
var body: some View {
ZStack {
Text("Example UI")
}
.withHostingWindow { window in
#if targetEnvironment(macCatalyst)
if let titlebar = window?.windowScene?.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
#endif
}
}
}
extension View {
fileprivate func withHostingWindow(_ callback: #escaping (UIWindow?) -> Void) -> some View {
self.background(HostingWindowFinder(callback: callback))
}
}
fileprivate struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
On your Scene, set .windowStyle(_:) to HiddenTitleBarWindowStyle().
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(HiddenTitleBarWindowStyle())
}
}
EDIT: Ah crap. While this API is supposedly available for Mac Catalyst according to the online documentation, it looks like it’s not actually marked as such in frameworks so you can’t use it.
What's the UIKit equivalent of the prefersHomeIndicatorAutoHidden property in SwiftUI?
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
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!
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()
}
}
}
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)
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.
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 EnvironmentObjects 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")
}
}
}