Where to place app delegate code in App.swift file? - swift

I'm trying to get Snapkit working with SwiftUI to allow logins via SnapChat. I'm following along with this StackOverflow question (Can I use the Snapchat SDK (SnapKit) with SwiftUI?) but I'm having trouble getting the accepted solution to work. The code posted as the answer was intended to go in the app delegate file but as of the latest version of XCode they are no longer used. Instead, the code snippet needs to be placed in the AppName.swift file but my breakpoint doesn't trigger. Here's my current version of my App.swift file:
import SwiftUI
import SCSDKLoginKit
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for urlContext in URLContexts {
let url = urlContext.url
var options: [UIApplication.OpenURLOptionsKey : Any] = [:]
options[.openInPlace] = urlContext.options.openInPlace
options[.sourceApplication] = urlContext.options.sourceApplication
options[.annotation] = urlContext.options.annotation
SCSDKLoginClient.application(UIApplication.shared, open: url, options: options)
}
}
Any help is greatly appreciated. Thanks!
EDIT: Here's the solution that worked thanks to Asperi! Updated code here in case anyone runs into this:
import SwiftUI
import SCSDKLoginKit
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
SCSDKLoginClient.application(UIApplication.shared, open: url)
}
}
}
}

You should use .onOpenURL instead, like
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
// .. do whatever needed here
}
}
}
}

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

What is equivalent to 'window.rootViewController' in WindowGroup - SwiftUI

I am new to SwiftUI and facing a problem where I want to change the root view when a certain action occurs inside the app.
How I handle it when using SceneDelegate was as follows
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
... // the code of initializing the window
observeOnChangeWindow()
}
func observeOnChangeWindow(){
NotificationCenter.default.addObserver(self, selector: #selector(self.performChangeWindow), name: Notification.Name(K.changeWindowNotificationName), object: nil)
}
#objc func performChangeWindow() {
self.window?.rootViewController = UIHostingController(rootView: SplashScreenView())
}
However, I am not currently using SceneDelegate as I am initializing the app using WindowGroup
struct MyApp: App {
var body: some Scene {
WindowGroup {
SplashScreenView()
}
}
}
My question is :
How can I perform the same thing I am doing using SceneDelegate now ?
With the help of comments and some tutorials, I reached out to the solution (Tested on iOS 15, Xcode 13.2.1):
Add the following code to the Main App Launcher.
struct MyApp: App {
#StateObject var appState = AppState.shared
var body: some Scene {
WindowGroup {
SplashScreenView().id(appState.environment)
}
}
}
And then I created the AppState class, which is the class that when changes I will change the window.
class AppState: ObservableObject {
static let shared = AppState()
#Published var environment = "Production"
}
And whenever I wanted to change the environment and do the same functionality of changing window in UIKit , do the following :
AppState.shared.environment = "Pre-Production"

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

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
}

Use ViewRouter in SwiftUI Project in Xcode 12

I tried to use this method (see link attached) in a new Xcode 12 project as a way to create a login page for a SwiftUI app, but I had the Problem not knowing what to add to the main App struct. I'm still a beginner and tried adding ContentView().environmentObject(ViewRouter()) to the WindowGroup in the main app struct. Am I totally wrong or why doesn't Xcode build the view? Can somebody help?
Below the working code snippet:
import SwiftUI
import Foundation
import Combine
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)
window.rootViewController = UIHostingController(rootView: MotherView().environmentObject(ViewRouter()))
self.window = window
window.makeKeyAndVisible()
}
}
. . .
}
class ViewRouter: ObservableObject {
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
var currentPage: String = "page1" {
didSet {
withAnimation() {
objectWillChange.send(self)
}
}
}
}
struct MotherView : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
VStack {
if viewRouter.currentPage == "page1" {
ContentViewA()
} else if viewRouter.currentPage == "page2" {
ContentViewB()
.transition(.scale)
}
}
}
}
struct ContentViewA : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Button(action: {self.viewRouter.currentPage = "page2"}) {
Text("Login")
}
}
}
struct ContentViewB : View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Button(action: {self.viewRouter.currentPage = "page1"}) {
Text("Logout")
}
}
}
Now I want to substitute the SceneDelegate in the Xcode 12 style, but the following doesn't work. Any idea why?
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
MotherView().environmentObject(ViewRouter())
}
}
}
Pls try next steps:
remove whole SceneDelegate class (in your case no need SceneDelegate class)
Modify your ViewRouter class like:
class ViewRouter: ObservableObject {
#Published var currentPage: String = "page1"
}