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
Related
Thank you for taking a look at this page. I am developing a Watch App and would like to do some background data processing in SwiftUI. However, the background stops after an hour at the most. how can I make it run for more than an hour?
#main
struct TestWatchOSOnly_Watch_AppApp: App {
#State var session = WKExtendedRuntimeSession()
//define and create the delegate
#State var delegate = WKDelegate()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear{
//create a new session
session = WKExtendedRuntimeSession()
//assign the delegate
session.delegate = delegate
//start the session
session.start()
}
}
}
}
// define the delegate and its methods
class WKDelegate: NSObject, WKExtendedRuntimeSessionDelegate{
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
print(reason.rawValue)
}
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("did start")
}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
print("will expire")
}
}
It is a private application for work and will not be made public. Also, I know that there is no mention of this in the official Apple documentation. Please let me know what possible ways to go about this.
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
}
This is my WatchDelegate.swift:
import WatchKit
class WatchDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
print("FINISHED LAUNCHING>>>>>>>>>>>") //not called
}
}
My info.plist:
My app is launching on my watch, but above delegate method is not called. Why?
In my case I didn't add anything to info.plist, I just added some magic swift sprinkles:
import SwiftUI
#main
struct WatchApp: App {
// 👇👇👇
#WKExtensionDelegateAdaptor(ExtensionDelegate.self) var delegate: ExtensionDelegate
// 👆👆👆
var body: some Scene {
WindowGroup {
// etc
I then see logging from
class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// stuff logged here shows in console
}
}
I'm trying to get my simulator setup to approve core location. A few times I've had the permission window actually pop up, but it quickly disappeared before I could select the "While In-Use" option.
I've tried the following based off of other similar questions I saw here and wenderlich walkthrough...
Features > Location > Apple
Info Plist has entries for...
Delete the app and:
Settings > General > Reset > Reset Location & Privacy
Using the below code on Xcode 12.4, Swift 5.1, iPhone SE - 2nd generation - iOS 14.4 simulator...
import SwiftUI
import CoreLocation
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
func setupCL() {
let locationManager = CLLocationManager()
let clDelegate = CLDelegate()
locationManager.delegate = clDelegate
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
}
var body: some View {
Button(action: {setupCL()}, label: {
Text("Button")
})
}
}
class CLDelegate: NSObject, CLLocationManagerDelegate { }
What am I missing?
Your locationManager and delegate are immediately going out of scope because you're calling them in a function on the view and not retaining them anywhere. Moving them to an ObservableObject makes everything work:
class Manager : ObservableObject {
let locationManager = CLLocationManager()
let clDelegate = CLDelegate()
func setupCL() {
locationManager.delegate = clDelegate
if locationManager.authorizationStatus == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
}
}
struct ContentView: View {
#StateObject private var manager = Manager()
var body: some View {
Button(action: {manager.setupCL()}) {
Text("Button")
}
}
}
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
}