Hide Title Bar in SwiftUI App for macCatalyst - ios

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.

Related

Forcing orientation between screens failing on iOS 16

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
}
}
}

How to change StatusBarStyle on SwiftUI App Lifecycle?

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.

UIScene userActivity is nil

I have a (Catalyst) app, that is able to have several windows. I created a button and if the user presses this button, the app creates a new window. To know, what View needs to be shown, I have different NSUserActivity activityTypes. In the new window there should be a button, that can close this new window. The problem is, that on looping through the open sessions, all the userActivities are nil (and I need the correct UISceneSession for the UIApplication.shared.requestSceneSessionDestruction.
This is my code:
struct ContentView: View {
var body: some View {
VStack {
// Open window type 1
Button(action: {
UIApplication.shared.requestSceneSessionActivation(nil,
userActivity: NSUserActivity(activityType: "window1"),
options: nil,
errorHandler: nil)
}) {
Text("Open new window - Type 1")
}
// Open window type 2
Button(action: {
UIApplication.shared.requestSceneSessionActivation(nil,
userActivity: NSUserActivity(activityType: "window2"),
options: nil,
errorHandler: nil)
}) {
Text("Open new window - Type 2")
}
}
}
}
class SceneDelegate: 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)
if connectionOptions.userActivities.first?.activityType == "window1" {
window.rootViewController = UIHostingController(rootView: Window1())
} else if connectionOptions.userActivities.first?.activityType == "window2" {
window.rootViewController = UIHostingController(rootView: Window2())
} else {
window.rootViewController = UIHostingController(rootView: ContentView())
}
self.window = window
window.makeKeyAndVisible()
}
}
...
struct Window1: View {
var body: some View {
VStack {
Text("Window1")
Button("show") {
for session in UIApplication.shared.openSessions {
// this prints two times nil !
print(session.scene?.userActivity?.activityType)
}
}
}
}
}
struct Window2: View {
var body: some View {
Text("Window2")
}
}
I ran into the same issue. I ended up assigning unique delegate classes to each scene and checking that instead.

Add google Sign-In with SwiftUI

I am trying to add google sign-in with swiftUI whith UIKitViewController, and for some reason I have difficulties showing the button does not appear in style
The signIn and the button works perfect but at some point the style of the button stopped appearing
I'm adding the button in a uikit viewcontroller, because I couldn't think of another way to handle the Google delegate
Here's the preview https://ibb.co/tYhx62b
//
// GoogleSignInButtonView.swift
//
// Created by Ivan Schaab on 11/09/2019.
// Copyright © 2019 Ivan Schaab. All rights reserved.
//
import GoogleSignIn
import SwiftUI
struct GoogleSignInButtonView: View {
#EnvironmentObject var lvm: LoginViewModel
var body: some View {
HStack {
Spacer()
GoogleButtonViewControllerRepresentable { (token, user) in
// Google Login Success
// Now do Backend Validations
self.lvm.loginOauth(token: token, user: user)
}
Spacer()
}.frame(alignment: .center)
}
}
class GoogleButtonUIKitViewController: UIViewController {
var signInButton = GIDSignInButton()
override func viewDidLoad() {
super.viewDidLoad()
GIDSignIn.sharedInstance().clientID = Constants.GOOGLE_CLIENT_ID
self.view.addSubview(signInButton)
GIDSignIn.sharedInstance()?.presentingViewController = self
// Automatically sign in the user.
GIDSignIn.sharedInstance()?.restorePreviousSignIn()
}
}
struct GoogleButtonViewControllerRepresentable: UIViewControllerRepresentable
{
let vc = GoogleButtonUIKitViewController()
var googleResponse: (String, User) -> Void
func makeUIViewController(context: Context) -> GoogleButtonUIKitViewController {
return vc
}
func updateUIViewController(_ uiViewController: GoogleButtonUIKitViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc, googleResponse: googleResponse)
}
static func dismantleUIViewController(_ uiViewController: GoogleButtonUIKitViewController, coordinator: GoogleButtonViewControllerRepresentable.Coordinator) {
print("DISMANTLE")
}
class Coordinator: NSObject, GIDSignInDelegate {
var foo: (String, User) -> Void
init(vc: GoogleButtonUIKitViewController, googleResponse: #escaping (String, User) -> Void) {
self.foo = googleResponse
super.init()
GIDSignIn.sharedInstance()?.delegate = self
}
func sign(_ signIn: GIDSignIn!, didSignInFor googleUser: GIDGoogleUser!, withError error: Error!) {
if let error = error {
if (error as NSError).code == GIDSignInErrorCode.hasNoAuthInKeychain.rawValue {
print("The user has not signed in before or they have since signed out.")
} else {
print("\(error.localizedDescription)")
}
return
}
// let userId = googleUser.userID // For client-side use only!
let idToken = googleUser.authentication.idToken // Safe to send to the server
let email = googleUser.profile.email
if googleUser.profile.hasImage{
let imageUrl = googleUser.profile.imageURL(withDimension: 120)
print(" image url: ", imageUrl?.absoluteString ?? "NO URL")
}
let user : User = User(id: 1, name: googleUser.profile.givenName, surname: googleUser.profile.familyName, imgName: "" , email: googleUser.profile.email)
print("email: ",email ?? "NO EMAIL")
foo(idToken! , user)
}
}
}
#if DEBUG
struct SomeRepView_Previews: PreviewProvider {
static var previews: some View {
GoogleSignInButtonView().environmentObject(LoginViewModel())
}
}
#endif
This is a part of my code where I implement Facebook and Google login, using any button I hope it helps you
import SwiftUI
import GoogleSignIn
import FBSDKLoginKit
var body: some View {
LoadingView(isShowing: .constant(loading)) {
NavigationView {
ScrollView {
VStack {
Text("Or Login with").font(.footnote)
HStack {
Button(action: self.logginFb, label: {
Image("ic_facebook").foregroundColor(Color.white).frame(width: 20, height: 20)
})
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color("facebook"))
.cornerRadius(8.0)
Button(action: self.socialLogin.attemptLoginGoogle, label: {
Image("ic_google").frame(width: 20, height: 20)
})
.padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.cornerRadius(8.0)
.shadow(radius: 4.0)
}.padding()
}.padding(.all, 32)
}.navigationBarTitle(Text("Login"))
}
}
}
func logginFb() {
socialLogin.attemptLoginFb(completion: { result, error in
})
}
struct SocialLogin: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<SocialLogin>) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<SocialLogin>) {
}
func attemptLoginGoogle() {
GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController
GIDSignIn.sharedInstance()?.signIn()
}
func attemptLoginFb(completion: #escaping (_ result: FBSDKLoginManagerLoginResult?, _ error: Error?) -> Void) {
let fbLoginManager: FBSDKLoginManager = FBSDKLoginManager()
fbLoginManager.logOut()
fbLoginManager.logIn(withReadPermissions: ["email"], from: UIApplication.shared.windows.last?.rootViewController) { (result, error) -> Void in
completion(result, error)
}
}
}
struct LoginView_Previews: PreviewProvider {
static var previews: some View {
LoginView()
}
}
The configuration of google and facebook in AppDelegate is the same as its documentation.
Now with swiftUI it is not necessary to use the default buttons that you give us.
Google and Facebook custom buttons
Note for SwiftUI 2.0 lifecycle: there is no standard AppDelegate so you also need to add an adapter in main App file:
#main
struct ExampleApp: App {
#UIApplicationDelegateAdaptor(ExampleAppDelegate.self) var appDelegate
and prepare the Delegate in a separate file:
//No #UIApplicationMain
class ExampleAppDelegate: NSObject, UIApplicationDelegate, GIDSignInDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GIDSignIn.sharedInstance().clientID = "YOUR CLINET ID"
GIDSignIn.sharedInstance().delegate = self
return true
}
...
With the latest SwiftUI, you can setup the GIDSignInDelegate within your AppDelegate and ensure that it conforms to all methods. Then, within your SceneDelegate, when setting up the window, you can drop this line in to setup the presenting view controller:
GIDSignIn.sharedInstance()?.presentingViewController = window.rootViewController.
Lastly, when creating your button, set the action or tap gesture to call GIDSignIn.sharedInstance().signIn() and you should be good to go!
GoogleSignIn 6.2.4 has the signIn function that requires a presenting UIViewController.
To solve this, add a sceneDelegate to the environment so it's accessible to SwiftUI.
Set up an AppDelegate so we can hook up our SceneDelegate
class AppDelegate: NSObject, UIApplicationDelegate {
// Hook up our SceneDelegate
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
}
}
Set up a SceneDelegate as an ObservableObject to keep track of the window
// Set this up so we can access the window in the environment
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
self.window = windowScene.keyWindow
}
}
Be sure to enable the AppDelegate in the App
#main
struct AdrichApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Finally, use it in a SwiftUI View to show your Continue with Google button
struct SignInWithGoogle: View {
#EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
if let vc = sceneDelegate.window?.rootViewController {
continueWithGoogle(config: GIDConfiguration(clientID: "YOUR CLINET ID"), presenter: vc)
} else {
emptyView
}
}
private var emptyView: some View {
print("Unable to access the root view controller")
return EmptyView()
}
private func continueWithGoogle(config: GIDConfiguration, presenter: UIViewController) -> some View {
Button {
GIDSignIn.sharedInstance.signIn(with: config, presenting: presenter) { user, error in
if let error = error {
print(error)
return
}
guard
let authentication = user?.authentication,
let idToken = authentication.idToken
else {
return
}
let credential = GoogleAuthProvider.credential(withIDToken: idToken, accessToken: authentication.accessToken)
// they are now signed in with Google!
}
} label: {
Text("Continue with Google")
}
}
}

How to hide the home indicator with SwiftUI?

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")
}
}
}

Resources