How do I disable the Show Tab Bar menu option in SwiftUI - swift

I have created a very simple app on MacOS created with SwiftUI. Default there is a menu item show tab bar. How do I remove this? I doesn't make sense to have tabs in this app.
I've found the following answering the same question, but for older versions of Swift, not for SwiftUI: How do I disable the Show Tab Bar menu option in Sierra apps?

The equivalent in SwiftUI is the same thing as the equivalent in Swift (which is missed in that post for some reason). To completely remove these items from any windows for your application, in your app delegate you can set the boolean value allowsAutomaticWindowTabbing to false
func applicationWillFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
}

Thanks to #JuJoDi
In case you are using SwiftUI life cycle (Scene), you could do the following:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
}
}

I was looking for an answer for this as well and found out the following:
by default - as you already mentioned - the Show/Hide Tab is active:
There is a property on NSWindow called tabbingMode which allows us to take control by setting it to .disallowed. My problem though was: in a SwiftUI 2-lifecycle app, how can I get hold of the windows of the app?
Well, there's NSApplication.shared.windows, so my first (non working!!) attempt was to modify all the windows in my #main-App struct (as I already prevented new windows from being created, that should be suffice):
import SwiftUI
#main
struct DQ_SyslogApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
Unfortunately this did not work as NSApplication.shared.windows is empty in .onAppear.
My next step involved introducing an AppDelegate to my SwiftUI 2-lifecycle that implements applicationDidFinishLaunching(_:)...
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
print("Info from `applicationDidFinishLaunching(_:): Finished launching…")
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
...and introducing this AppDelegate to the app:
import SwiftUI
#main
struct DQ_SyslogApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
This did the trick for me:
While I would prefer to have the menu entries removed entirely, this at least prevents the user from having tabs which is what the OP was asking for.
If anybody should happen to know a solution to hide those entries, please let us know. I couldn't find a CommandGroupPlacement that represents these menus...

I found how to remove the 'Edit' menu item after reading this article
https://steipete.com/posts/top-level-menu-visibility-in-swiftui/
func applicationDidFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
if let mainMenu = NSApp .mainMenu {
DispatchQueue.main.async {
if let edit = mainMenu.items.first(where: { $0.title == "Edit"}) {
mainMenu.removeItem(edit);
}
}
}
}

2021, swiftUI 2 and higher
Result in release build:
look at places with comments "HERE":
#main
struct MyFirstApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
// HERE
disallowTabbingMode()
}
#SceneBuilder
var body: some Scene {
WindowGroup {
AppMainView()
}
.commands {
//MenuLine_MyFirstApp_CustomAbout()
// HERE
MenuLine_File_NewWindow_Disable()
//MenuLine_Help_SupportEmail()
}
}
}
And method + bonuses for understanding menus in SwiftUI
// Set Custom About window instead of default
fileprivate func MenuLine_MyFirstApp_CustomAbout() -> CommandGroup<Button<Text>> {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("About") { appDelegate.showAboutWnd() }
}
}
//Add some menu button into Help menu
fileprivate func MenuLine_Help_SupportEmail() -> CommandGroup<Button<Text>> {
CommandGroup(after: CommandGroupPlacement.help) {
Button("Support Email") { NSWorkspace.shared.open("mailto:support#myfirstapp.com") }
}
}
// Disable "File -> New window" menu item (make it absent in release build)
fileprivate func MenuLine_File_NewWindow_Disable() -> CommandGroup<EmptyView> {
CommandGroup(replacing: .newItem) { }
}
fileprivate func disallowTabbingMode() {
NSWindow.allowsAutomaticWindowTabbing = false
}

Related

How can I disable HideTabBar and ShowAllTabs in macOS in SwiftUI? [duplicate]

I have created a very simple app on MacOS created with SwiftUI. Default there is a menu item show tab bar. How do I remove this? I doesn't make sense to have tabs in this app.
I've found the following answering the same question, but for older versions of Swift, not for SwiftUI: How do I disable the Show Tab Bar menu option in Sierra apps?
The equivalent in SwiftUI is the same thing as the equivalent in Swift (which is missed in that post for some reason). To completely remove these items from any windows for your application, in your app delegate you can set the boolean value allowsAutomaticWindowTabbing to false
func applicationWillFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
}
Thanks to #JuJoDi
In case you are using SwiftUI life cycle (Scene), you could do the following:
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
}
}
I was looking for an answer for this as well and found out the following:
by default - as you already mentioned - the Show/Hide Tab is active:
There is a property on NSWindow called tabbingMode which allows us to take control by setting it to .disallowed. My problem though was: in a SwiftUI 2-lifecycle app, how can I get hold of the windows of the app?
Well, there's NSApplication.shared.windows, so my first (non working!!) attempt was to modify all the windows in my #main-App struct (as I already prevented new windows from being created, that should be suffice):
import SwiftUI
#main
struct DQ_SyslogApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
Unfortunately this did not work as NSApplication.shared.windows is empty in .onAppear.
My next step involved introducing an AppDelegate to my SwiftUI 2-lifecycle that implements applicationDidFinishLaunching(_:)...
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
print("Info from `applicationDidFinishLaunching(_:): Finished launching…")
let _ = NSApplication.shared.windows.map { $0.tabbingMode = .disallowed }
}
}
...and introducing this AppDelegate to the app:
import SwiftUI
#main
struct DQ_SyslogApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandGroup(replacing: .newItem) {} //remove "New Item"-menu entry
}
}
}
This did the trick for me:
While I would prefer to have the menu entries removed entirely, this at least prevents the user from having tabs which is what the OP was asking for.
If anybody should happen to know a solution to hide those entries, please let us know. I couldn't find a CommandGroupPlacement that represents these menus...
I found how to remove the 'Edit' menu item after reading this article
https://steipete.com/posts/top-level-menu-visibility-in-swiftui/
func applicationDidFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
if let mainMenu = NSApp .mainMenu {
DispatchQueue.main.async {
if let edit = mainMenu.items.first(where: { $0.title == "Edit"}) {
mainMenu.removeItem(edit);
}
}
}
}
2021, swiftUI 2 and higher
Result in release build:
look at places with comments "HERE":
#main
struct MyFirstApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
init() {
// HERE
disallowTabbingMode()
}
#SceneBuilder
var body: some Scene {
WindowGroup {
AppMainView()
}
.commands {
//MenuLine_MyFirstApp_CustomAbout()
// HERE
MenuLine_File_NewWindow_Disable()
//MenuLine_Help_SupportEmail()
}
}
}
And method + bonuses for understanding menus in SwiftUI
// Set Custom About window instead of default
fileprivate func MenuLine_MyFirstApp_CustomAbout() -> CommandGroup<Button<Text>> {
CommandGroup(replacing: CommandGroupPlacement.appInfo) {
Button("About") { appDelegate.showAboutWnd() }
}
}
//Add some menu button into Help menu
fileprivate func MenuLine_Help_SupportEmail() -> CommandGroup<Button<Text>> {
CommandGroup(after: CommandGroupPlacement.help) {
Button("Support Email") { NSWorkspace.shared.open("mailto:support#myfirstapp.com") }
}
}
// Disable "File -> New window" menu item (make it absent in release build)
fileprivate func MenuLine_File_NewWindow_Disable() -> CommandGroup<EmptyView> {
CommandGroup(replacing: .newItem) { }
}
fileprivate func disallowTabbingMode() {
NSWindow.allowsAutomaticWindowTabbing = false
}

Hiding Edit Menu of a SwiftUI / MacOS app

My MacOS app doesn't have any text editing possibilities. How can I hide the Edit menu which is added to my app automatically? I'd prefer to do this in SwiftUI.
I would expect the code below should work, but it doesn't.
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
CommandGroup(replacing: .textEditing) {}
}
}
}
To my knowledge you cannot hide the whole menu, you can just hide element groups inside of it:
.commands {
CommandGroup(replacing: .pasteboard) { }
CommandGroup(replacing: .undoRedo) { }
}
For native (Cocoa) apps
It is possible to remove application menus using an NSApplicationDelegate. This approach may break in future macOS versions (e.g. if the position of the Edit menu were to change), but does currently work:
class MyAppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
let indexOfEditMenu = 2
func applicationDidFinishLaunching(_ : Notification) {
NSApplication.shared.mainMenu?.removeItem(at: indexOfEditMenu)
}
}
#main
struct MyApp: App {
#NSApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
For Catalyst (UIKit) apps
For Catalyst-based macOS apps, the approach is similar to that above, except that a UIApplicationDelegate deriving from UIResponder is used:
class MyAppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
override func buildMenu(with builder: UIMenuBuilder) {
/// Only operate on the main menu bar.
if builder.system == .main {
builder.remove(menu: .edit)
}
}
}
#main
struct MyApp: App {
#UIApplicationDelegateAdaptor private var appDelegate: MyAppDelegate
var body: some Scene {
WindowGroup {
ContentView()
}.commands {
// ...
}
}
}
The current suggestions failed for me when SwiftUI updated the body of a window.
Solution:
Use KVO and watch the NSApp for changes on \.mainMenu. You can remove whatever you want after SwiftUI has its turn.
#objc
class AppDelegate: NSObject, NSApplicationDelegate {
var token: NSKeyValueObservation?
func applicationDidFinishLaunching(_ notification: Notification) {
// Remove a single menu
if let m = NSApp.mainMenu?.item(withTitle: "Edit") {
NSApp.mainMenu?.removeItem(m)
}
// Remove Multiple Menus
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Must remove after every time SwiftUI re adds
token = NSApp.observe(\.mainMenu, options: .new) { (app, change) in
["Edit", "View", "Help", "Window"].forEach { name in
NSApp.mainMenu?.item(withTitle: name).map { NSApp.mainMenu?.removeItem($0) }
}
// Remove a single menu
guard let menu = app.mainMenu?.item(withTitle: "Edit") else { return }
app.mainMenu?.removeItem(menu)
}
}
}
struct MarblesApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some View {
//...
}
}
Thoughts:
SwiftUI either has a bug or they really don't want you to remove the top level menus in NSApp.mainMenu. SwiftUI seems to reset the whole menu with no way to override or customize most details currently (Xcode 13.4.1). The CommandGroup(replacing: .textEditing) { }-esque commands don't let you remove or clear a whole menu. Assigning a new NSApp.mainMenu just gets clobbered when SwiftUI wants even if you specify no commands.
This seems like a very fragile solution. There should be a way to tell SwiftUI to not touch the NSApp.mainMenu or enable more customization. Or it seems SwiftUI should check that it owned the previous menu (the menu items are SwiftUI.AppKitMainMenuItem). Or I'm missing some tool they've provided. Hopefully this is fixed in the WWDC beta?
(In Xcode 13.4.1 with Swift 5 targeting macOS 12.3 without Catalyst.)
For those of you that are looking for any updates on this - have a look at this question that I asked (and answered myself):
SwiftUI Update the mainMenu [SOLVED] kludgey
The way I got around it was to put it in a DispatchQueue.main.async closure in the AppDelegate applicationWillUpdate function
import Foundation
import AppKit
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationWillUpdate(_ notification: Notification) {
DispatchQueue.main.async {
let currentMainMenu = NSApplication.shared.mainMenu
let editMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "Edit")
if nil != editMenu {
NSApp.mainMenu?.removeItem(editMenu!)
}
}
}
}
It took me a good 4 days of searching and attempting things :) - typical that it came down to a 2 line code change

Create a windowless SwiftUI macOS application?

I'm creating a menu bar app for macOS using SwiftUI.I have figured out how to create a menu bar item, and how to hide the icon from the Dock. But there is one thing left I need to figure out to have a proper menu bar app, and that is how to not show a window on screen (see screenshot).
When using SwiftUI for the #main App class, it looks like I have to return a WindowGroup with some content. I've tried EmptyView() etc. but it has to be "some view that conforms to Scene".
What I have
Here is the code I have so far.
import SwiftUI
import Combine
class AppViewModel: ObservableObject {
#Published var showPopover = false
}
#main struct WeeksApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup { Text("I don't want to show this window. 🥲") }
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
print("Background")
case .active:
print("Active")
case .inactive:
print("Inactive")
#unknown default:
fatalError()
}
}
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var popover = NSPopover.init()
var statusBarItem: NSStatusItem?
var viewModel = AppViewModel()
var cancellable: AnyCancellable?
override init() {
super.init()
cancellable = viewModel.objectWillChange.sink { [weak self] in
if (self?.viewModel.showPopover == false) {
self?.closePopover(self)
}
}
}
func applicationDidFinishLaunching(_ notification: Notification) {
popover.behavior = .transient
popover.animates = false
popover.contentViewController = NSViewController()
popover.contentViewController?.view = NSHostingView(rootView: ContentView(viewModel: viewModel))
statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusBarItem?.button?.title = "Week \(getCurrentWeekNumber())"
statusBarItem?.button?.action = #selector(AppDelegate.togglePopover(_:))
}
#objc func showPopover(_ sender: AnyObject?) {
if let button = statusBarItem?.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
}
#objc func closePopover(_ sender: AnyObject?) {
popover.performClose(sender)
}
#objc func togglePopover(_ sender: AnyObject?) {
if popover.isShown {
closePopover(sender)
} else {
showPopover(sender)
}
}
func getCurrentWeekNumber() -> Int {
let calendar = Calendar.current
let weekOfYear = calendar.component(.weekOfYear, from: Date())
return weekOfYear
}
}
So how can I launch the app without showing a window? I would prefer a SwiftUI solution. I know it can be done the "old" way.
I'm not sure if it's a hack and if there is a better way. I wrote an app as an agent, with the possibility to show a window later in the process. However, at startup, the app should not show a window.
I made an application delegate with the following code:
class Appdelegate: NSObject, NSApplicationDelegate {
#Environment(\.openURL) var openURL
var statusItem: NSStatusItem!
func applicationDidFinishLaunching(_ notification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button!.image = NSImage(named: "AppIcon")
statusItem.isVisible = true
statusItem.menu = NSMenu()
addConfigrationMenuItem()
BundleLocator().checkExistingLaunchAgent()
if let window = NSApplication.shared.windows.first {
window.close()
}
}
}
Notice at the end I close the window, which let my app start in as a menubar app only. However, I do have a content view that can be opened later. The app therefore looks like this:
#main
struct RestartAtDateTryoutApp: App {
#NSApplicationDelegateAdaptor(Appdelegate.self) var appDelegate
var scheduler = Scheduler()
var body: some Scene {
WindowGroup {
ContentView()
.handlesExternalEvents(preferring: Set(arrayLiteral: "ContentView"), allowing: Set(arrayLiteral: "*"))
.environmentObject(scheduler)
.frame(width: 650, height: 450)
}
.handlesExternalEvents(matching: Set(arrayLiteral: "ContentView"))
}
}
This gives me the menubar app with items of which one is a window with a user interface.
I hope this also works in your app.

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.

Programmatically navigate to new view in SwiftUI

Descriptive example:
login screen, user taps "Login" button, request is performed, UI shows waiting indicator, then after successful response I'd like to automatically navigate user to the next screen.
How can I achieve such automatic transition in SwiftUI?
You can replace the next view with your login view after a successful login. For example:
struct LoginView: View {
var body: some View {
...
}
}
struct NextView: View {
var body: some View {
...
}
}
// Your starting view
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}
}
}
You should handle your login process in your data model and use bindings such as #EnvironmentObject to pass isLoggedin to your view.
Note: In Xcode Version 11.0 beta 4, to conform to protocol 'BindableObject' the willChange property has to be added
import Combine
class UserAuth: ObservableObject {
let didChange = PassthroughSubject<UserAuth,Never>()
// required to conform to protocol 'ObservableObject'
let willChange = PassthroughSubject<UserAuth,Never>()
func login() {
// login request... on success:
self.isLoggedin = true
}
var isLoggedin = false {
didSet {
didChange.send(self)
}
// willSet {
// willChange.send(self)
// }
}
}
For future reference, as a number of users have reported getting the error "Function declares an opaque return type", to implement the above code from #MoRezaFarahani requires the following syntax:
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
return AnyView(LoginView())
} else {
return AnyView(NextView())
}
}
}
This is working with Xcode 11.4 and Swift 5
struct LoginView: View {
#State var isActive = false
#State var attemptingLogin = false
var body: some View {
ZStack {
NavigationLink(destination: HomePage(), isActive: $isActive) {
Button(action: {
attlempinglogin = true
// Your login function will most likely have a closure in
// which you change the state of isActive to true in order
// to trigger a transition
loginFunction() { response in
if response == .success {
self.isActive = true
} else {
self.attemptingLogin = false
}
}
}) {
Text("login")
}
}
WaitingIndicator()
.opacity(attemptingLogin ? 1.0 : 0.0)
}
}
}
Use Navigation link with the $isActive binding variable
To expound what others have elaborated above based on changes on combine as of Swift Version 5.2 it could be simplified using publishers.
Create a class names UserAuth as shown below don't forget to import import Combine.
class UserAuth: ObservableObject {
#Published var isLoggedin:Bool = false
func login() {
self.isLoggedin = true
}
}
Update SceneDelegate.Swift with
let contentView = ContentView().environmentObject(UserAuth())
Your authentication view
struct LoginView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
...
if ... {
self.userAuth.login()
} else {
...
}
}
}
Your dashboard after successful authentication, if the authentication userAuth.isLoggedin = true then it will be loaded.
struct NextView: View {
var body: some View {
...
}
}
Lastly, the initial view to be loaded once the application is launched.
struct ContentView: View {
#EnvironmentObject var userAuth: UserAuth
var body: some View {
if !userAuth.isLoggedin {
LoginView()
} else {
NextView()
}
}
}
Here is an extension on UINavigationController that has simple push/pop with SwiftUI views that gets the right animations. The problem I had with most custom navigations above was that the push/pop animations were off. Using NavigationLink with an isActive binding is the correct way of doing it, but it's not flexible or scalable. So below extension did the trick for me:
/**
* Since SwiftUI doesn't have a scalable programmatic navigation, this could be used as
* replacement. It just adds push/pop methods that host SwiftUI views in UIHostingController.
*/
extension UINavigationController: UINavigationControllerDelegate {
convenience init(rootView: AnyView) {
let hostingView = UIHostingController(rootView: rootView)
self.init(rootViewController: hostingView)
// Doing this to hide the nav bar since I am expecting SwiftUI
// views to be wrapped in NavigationViews in case they need nav.
self.delegate = self
}
public func pushView(view:AnyView) {
let hostingView = UIHostingController(rootView: view)
self.pushViewController(hostingView, animated: true)
}
public func popView() {
self.popViewController(animated: true)
}
public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
navigationController.navigationBar.isHidden = true
}
}
Here is one quick example using this for the window.rootViewController.
var appNavigationController = UINavigationController.init(rootView: rootView)
window.rootViewController = appNavigationController
window.makeKeyAndVisible()
// Now you can use appNavigationController like any UINavigationController, but with SwiftUI views i.e.
appNavigationController.pushView(view: AnyView(MySwiftUILoginView()))
I followed Gene's answer but there are two issues with it that I fixed below. The first is that the variable isLoggedIn must have the property #Published in order to work as intended. The second is how to actually use environmental objects.
For the first, update UserAuth.isLoggedIn to the below:
#Published var isLoggedin = false {
didSet {
didChange.send(self)
}
The second is how to actually use Environmental objects. This isn't really wrong in Gene's answer, I just noticed a lot of questions about it in the comments and I don't have enough karma to respond to them. Add this to your SceneDelegate view:
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).
var userAuth = UserAuth()
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().environmentObject(userAuth)
Now you need to just simply create an instance of the new View you want to navigate to and put that in NavigationButton:
NavigationButton(destination: NextView(), isDetail: true, onTrigger: { () -> Bool in
return self.done
}) {
Text("Login")
}
If you return true onTrigger means you successfully signed user in.