How to push a new root view using SwiftUI without NavigationLink? - ios

I have a login screen. After user fill up credential, I want to verify it then start a new root view so user won't be able to navigate back to the login view.
I currently have
Button(action: {
// launch new root view here
}, label: {Text("Login")}).padding()
Majority of answers I found online are using navigation link which I don't want to. Some other answers suggest to utilize AppDelegate by UIApplication.shared.delegate which isn't working for me because I have SceneDelegate

Here is possible alternate approach of how to replace root view completely... using notifications
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let loginRootViewNotification =
NSNotification.Name("loginRootViewNotification") // declare notification
private var observer: Any? // ... and observer
...
// in place of window creation ...
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
observer = NotificationCenter.default.addObserver(forName: loginRootViewNotification, object: nil, queue: nil, using: { _ in
let anotherRootView = AnotherRootView()
// create another view on notification and replace
window.rootViewController = UIHostingController(rootView: anotherRootView)
})
in your desired place post needed notification
Button(action: {
// launch new root view here
NotificationCenter.default.post(loginRootViewNotification)
}, label: {Text("Login")}).padding()

Another approach with Notifications is to place the observer inside your RootView and check for changes in a variable to decide which View should be presented, here is a very simplified example:
struct RootView: View {
#State var isLoggedIn: Bool = false
var body: some View {
Group {
VStack{
if isLoggedIn {
Text("View B")
Button(action: {
NotificationCenter.default.post(name: NSNotification.Name("changeLogin"), object: nil)
}) {
Text("Logout")
}
} else {
Text("View A")
Button(action: {
NotificationCenter.default.post(name: NSNotification.Name("changeLogin"), object: nil)
}) {
Text("Login")
}
}
}
}.onAppear {
NotificationCenter.default.addObserver(forName: NSNotification.Name("changeLogin"), object: nil, queue: .main) { (_) in
self.isLoggedIn.toggle()
}
}
}
And load RootView in your rootViewController as usual.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = RootView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}

Related

Widget link works but does nothing

I have a widget with a link. I'm implementing the widget URL in SceneDelegate. The problem is when I tap on the widget I get a link, it works. But when I call View Controller function in SceneDelegate, it does nothing. Well, it can print something, but it doesn't change anything. SceneDelegate:
// App launched
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ViewController()
self.window = window
window.makeKeyAndVisible()
getURL(urlContext: connectionOptions.urlContexts)
}
// App opened from background
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
getURL(urlContext: URLContexts)
}
// Get URL
private func getURL(urlContext: Set<UIOpenURLContext>) {
guard let urlContexts = urlContext.first(where: { $0.url.scheme == "app-deeplink" }) else { return }
print(urlContexts.url)
print("success")
let viewController = ViewController()
viewController.cameFromWidget()
}
Function in ViewController that changes labels text:
func cameFromWidget() {
label.text = "Hello"
print(label.text)
}
WidgetExtension:
var body: some View {
Text(entry.date, style: .time)
.widgetURL(URL(string: "app-deeplink://app"))
}
So ViewController func just prints the text but doesn't change it when I call it from SceneDelegate.
My widget link is only in WidgetExtension and SceneDelegate, I didn't add it to infoPlist.
My question: Why does it work but does nothing? Maybe I should add it to some file?
Thank you so much!
I found out. To make some changes in your viewController you need to make it with rootViewController:
private func getURL(urlContext: Set<UIOpenURLContext>) {
guard let urlContexts = urlContext.first(where: { $0.url.scheme == "app-deeplink" }) else { return }
print(urlContexts.url)
print("success")
let rootViewController = window?.rootViewController as? ViewController
rootViewController?.cameFromWidget()
}

How to auto-move/auto-adjust the save button for a note-taking app in swift?

This is what the app looks like when first opened:
I am content with this as the first screen the user sees.
The problem arises when the user hits the plus button to create a new note. The user touches the screen to add a new note and the keyboard covers up the save button at the bottom right corner. Is there any way to to have button appear above the keyboard on the top right side?
This is what it looks like:
I simply want the plus button above the keyboard on the right side so that the user can save the note. How would this be done?
Please help. Thank you!
Here is the entire code:
AppDelegate.swift:
import UIKit
import Firebase
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
FirebaseApp.configure()
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
SceneDelegate.swift:
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = Host(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
ContentView.swift:
import SwiftUI
import Firebase
struct ContentView: View {
var body: some View {
Home()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Home : View {
#ObservedObject var Notes = getNotes()
#State var show = false
#State var txt = ""
#State var docID = ""
#State var remove = false
var body : some View{
ZStack(alignment: .bottomTrailing) {
VStack(spacing: 0){
HStack{
Text("Notes").font(.title).foregroundColor(.white)
Spacer()
Button(action: {
self.remove.toggle()
}) {
Image(systemName: self.remove ? "xmark.circle" : "trash").resizable().frame(width: 23, height: 23).foregroundColor(.white)
}
}.padding()
.padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top)
.background(Color.red)
if self.Notes.data.isEmpty{
if self.Notes.noData{
Spacer()
Text("No Notes...")
Spacer()
}
else{
Spacer()
//Data is Loading ....
Indicator()
Spacer()
}
}
else{
ScrollView(.vertical, showsIndicators: false) {
VStack{
ForEach(self.Notes.data){i in
HStack(spacing: 15){
Button(action: {
self.docID = i.id
self.txt = i.note
self.show.toggle()
}) {
VStack(alignment: .leading, spacing: 12){
Text(i.date)
Text(i.note).lineLimit(1)
Divider()
}.padding(10)
.foregroundColor(.black)
}
if self.remove{
Button(action: {
let db = Firestore.firestore()
db.collection("notes").document(i.id).delete()
}) {
Image(systemName: "minus.circle.fill")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.red)
}
}
}.padding(.horizontal)
}
}
}
}
}.edgesIgnoringSafeArea(.top)
Button(action: {
self.txt = ""
self.docID = ""
self.show.toggle()
}) {
Image(systemName: "plus").resizable().frame(width: 18, height: 18).foregroundColor(.white)
}.padding()
.background(Color.red)
.clipShape(Circle())
.padding()
}
.sheet(isPresented: self.$show) {
EditView(txt: self.$txt, docID: self.$docID, show: self.$show)
}
.animation(.default)
}
}
class Host : UIHostingController<ContentView>{
override var preferredStatusBarStyle: UIStatusBarStyle{
return .lightContent
}
}
class getNotes : ObservableObject{
#Published var data = [Note]()
#Published var noData = false
init() {
let db = Firestore.firestore()
db.collection("notes").order(by: "date", descending: false).addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
self.noData = true
return
}
if (snap?.documentChanges.isEmpty)!{
self.noData = true
return
}
for i in snap!.documentChanges{
if i.type == .added{
let id = i.document.documentID
let notes = i.document.get("notes") as! String
let date = i.document.get("date") as! Timestamp
let format = DateFormatter()
format.dateFormat = "MM/YY"
let dateString = format.string(from: date.dateValue())
self.data.append(Note(id: id, note: notes, date: dateString))
}
if i.type == .modified{
// when data is changed...
let id = i.document.documentID
let notes = i.document.get("notes") as! String
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data[i].note = notes
}
}
}
if i.type == .removed{
// when data is removed...
let id = i.document.documentID
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data.remove(at: i)
if self.data.isEmpty{
self.noData = true
}
return
}
}
}
}
}
}
}
struct Note : Identifiable {
var id : String
var note : String
var date : String
}
struct Indicator : UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<Indicator>) -> UIActivityIndicatorView {
let view = UIActivityIndicatorView(style: .medium)
view.startAnimating()
return view
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<Indicator>) {
}
}
struct EditView : View {
#Binding var txt : String
#Binding var docID : String
#Binding var show : Bool
var body : some View{
ZStack(alignment: .bottomTrailing) {
MultiLineTF(txt: self.$txt)
.padding()
.background(Color.black.opacity(0.05))
Button(action: {
self.show.toggle()
self.SaveData()
}) {
Text("Save").padding(.vertical).padding(.horizontal,25).foregroundColor(.white)
}.background(Color.red)
.clipShape(Capsule())
.padding()
}.edgesIgnoringSafeArea(.bottom)
}
func SaveData(){
let db = Firestore.firestore()
if self.docID != ""{
db.collection("notes").document(self.docID).updateData(["notes":self.txt]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
}
}
else{
db.collection("notes").document().setData(["notes":self.txt,"date":Date()]) { (err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
}
}
}
}
struct MultiLineTF : UIViewRepresentable {
func makeCoordinator() -> MultiLineTF.Coordinator {
return MultiLineTF.Coordinator(parent1: self)
}
#Binding var txt : String
func makeUIView(context: UIViewRepresentableContext<MultiLineTF>) -> UITextView{
let view = UITextView()
if self.txt != ""{
view.text = self.txt
view.textColor = .black
}
else{
view.text = "Type Something"
view.textColor = .gray
}
view.font = .systemFont(ofSize: 18)
view.isEditable = true
view.backgroundColor = .clear
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<MultiLineTF>) {
}
class Coordinator : NSObject,UITextViewDelegate{
var parent : MultiLineTF
init(parent1 : MultiLineTF) {
parent = parent1
}
func textViewDidBeginEditing(_ textView: UITextView) {
if self.parent.txt == ""{
textView.text = ""
textView.textColor = .black
}
}
func textViewDidChange(_ textView: UITextView) {
self.parent.txt = textView.text
}
}
}
Remove your .edgesIgnoringSafeArea(.bottom) attached to the ZStack.
The safe area includes the area taken up by the keyboard. Right now, because you've explicitly told it to ignore the safe area, it's not moving to accommodate the keyboard. If it respects the safe area, the button will move with the keyboard.

How to get value on original object with EnvironmentObject in swiftUI

class GameSettings: ObservableObject {
#Published var score = 0
#Published var score1:Int? = 0
}
struct ScoreView: View {
#EnvironmentObject var settings: GameSettings
var body: some View {
NavigationView {
NavigationLink(destination: ContentView3()) {
Text("Score: \(settings.score)")
}
}
}
}
struct ContentView3: View {
#StateObject var settings = GameSettings()
#EnvironmentObject var settings111: GameSettings
var body: some View {
NavigationView {
VStack {
// A button that writes to the environment settings
Text("Current Score--->\(settings.score))")
Text(settings111.score1 == nil ? "nil" : "\(settings111.score1!)")
Button("Increase Score") {
settings.score += 1
}
NavigationLink(destination: ScoreView()) {
Text("Show Detail View")
}
}
.frame(height: 200)
}
.environmentObject(settings)
}
}
So here When user has performed some changes in ContentView3 & from navigation route if user lands to same screen i.e. ContentView3 so how can I get GameSettings object latest value on it ? I tried creating #EnvironmentObject var settings111: GameSettings but crashes.
Did you add .environmentObject() to your YourApp.swift as well?
If not, you have to add it like this
Life Cycle: SwiftUI
#main
struct YourApp: App {
var settings: GameSettings = GameSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings)
}
}
}
Life Cycle: UIKit
In SceneDelegate.swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
var settings: GameSettings = GameSettings() // added line
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings))) // added ".environmentObject(settings)" after contentView
self.window = window
window.makeKeyAndVisible()
}
}
Preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(GameSettings())
}
}

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.

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