When I fire up my OS X app from Xcode, it appears as it should.
When I close the window, it disappears (as you would expect), and the app still appears active on the dock.
Great, that's how it should be. But when I click the dock to activate the window, it doesn't show up.
Any ideas?
There isn't much information in the question, but let me presume that the app is a single-window app (i.e., not NSDocument-based).
A typical situation in this scenario is that the user closes the window, and the appliction keeps running with the icon in the Dock as it is supposed to.
In that situation, the user would normally like the window to re-appear when the app is activated by clicking the icon in the Dock.
To obtain that, you can implement applicationShouldHandleReopen:hasVisibleWindows: as follows:
- (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)visibleWindows
{
if ( visibleWindows ) {
[self.window orderFront:self];
}
else {
[self.window makeKeyAndOrderFront:self];
}
return YES;
}
for swift:
func applicationShouldHandleReopen(sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if let window = sender.windows.first {
if flag {
window.orderFront(nil)
} else {
window.makeKeyAndOrderFront(nil)
}
}
return true
}
Related
The Real Question
How do you update the mainMenu in SwiftUI so that it actually works?
I have built a MacOS Document Based application in SwiftUI which includes all of the in-built File menu commands (i.e. Close, Save, Duplicate. Rename... etc.)
Before saving the document, I validate the structure and would like to present a modal dialog to the user if there are any validation errors.
The modal dialog is just a simple OK/Cancel dialog - 'OK' meaning that the user is happy to save the file with validation errors, 'Cancel' would need to stop the save operation.
So the question is: "How do I intercept the in-built 'Save' menu command to present this dialog?
I have tried to overwrite the .saveItem CommandGroup - but this replaces all of the menu items and I only want to override a couple of the commands ('Save' and 'Save As') and don't want to re-implement them all (and I am not sure that I have the skills to do so)
.commands {
CommandGroup(replacing: .saveItem) {
// code goes here - but removes all of the in-built menus
}
}
I have tried this solution (In a SwiftUI Document App, how to save a document from within a function)
and have put it into my AppDelegate
public func applicationDidBecomeActive(_ notification: Notification) {
let menu = NSApplication.shared.mainMenu!.items.first(where: { $0.title == "File" })!
let submenu = menu.submenu!.items.first(where: { $0.title == "Save" })!
submenu.action = #selector(showDialog)
}
#objc func showDialog() {
var retVal: Int = 0
let thisWindow: NSWindow? = NSApplication.shared.mainWindow
let nsAlert: NSAlert = NSAlert()
let cancelButton: NSButton = nsAlert.addButton(withTitle: "Cancel")
cancelButton.tag = 1
let okButton: NSButton = nsAlert.addButton(withTitle: "OK")
okButton.tag = 0
// The below code is replaced
nsAlert.beginSheetModal(for: thisWindow!) { modalResponse in
print(modalResponse)
retVal = modalResponse.rawValue
if retVal == 0 {
print("save")
} else {
print("cancel")
}
}
}
However it doesn't actually call the showDialog function.
Edit/Update
I am still having difficulties updating the menus, but in the above example the call to beginModalSheet is incorrect as the process will run in the background. Updated the call to runModal() which will stop any background process writing the file.
#objc func showDialog() {
let nsAlert: NSAlert = NSAlert()
let cancelButton: NSButton = nsAlert.addButton(withTitle: "Cancel")
cancelButton.tag = 1
let okButton: NSButton = nsAlert.addButton(withTitle: "OK")
okButton.tag = 0
let response: Int = nsAlert.runModal().rawValue
if response == 0 {
print("save")
NSApp.sendAction(#selector(NSDocument.save(_:)), to: nil, from: nil)
} else {
print("cancel")
}
}
I have read somewhere that you need to set the menu before the window appears, and I have also read that you need to set the menus before the AppDelegate is set.
Yet another edit
See this post Hiding Edit Menu of a SwiftUI / MacOS app
and this comment
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.
XCode 14.1
Swift 5
After a lot of super frustrating searching an attempts and lots of code - I reduced the problem to being just trying to change the name of the save menu item - If I could do this - then I can change the action for it as well.
Here is how I did it
My Tetsing App is called YikesRedux
Steps:
Register the AppDelegate
Override the applicationWillUpdate method
Put the menu updating in a DispatchQueue.main.async closure
Cry tears of joy that you have solved this problem after days of searching
YikesAppRedux.swift
import SwiftUI
#main
struct YikesReduxApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate // <- Don't forget the AppDelegate
var body: some Scene {
DocumentGroup(newDocument: YikesReduxDocument()) { file in
ContentView(document: file.$document)
}
}
}
AppDelegate.swift
import Foundation
import AppKit
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationWillUpdate(_ notification: Notification) {
DispatchQueue.main.async {
let currentMainMenu = NSApplication.shared.mainMenu
let fileMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "File")
if nil != fileMenu {
let saveMenu = fileMenu?.submenu!.item(withTitle: "Save")
if nil != saveMenu {
print("updated menu")
saveMenu?.title = "Save Updated"
}
}
}
}
}
I put this down as a bit kludgey - as it runs on every application update (which is not a lot, but you can see the print out in the console "updated menu" when it does occur)
I did try to keep a state variable as to whether the menu was updated, to try and not do it again - but in a multi document window environment you would need to keep track of every window... (Also swift just clobbers the menu whenever it wants - so it didn't work as well as expected.)
I put the menu updating code in almost everywhere I could think of
Every single AppDelegate function override
init methods for the App, the ContentView
on the document read function/write function
You name it - I put it in there (I even had a hosting controller, a NSViewRepresentable)
I then removed them one by one until I found the solution.
I would be happy if there was a less kludgey way to do this.
I am working on a MacOS app that can process images. The idea is the app hides out of the way most of the time, but if you drag an image from another app or the finder, my app will show and you can drag the image on top of it. Basically a drop zone on standby for when you need it.
Everything is working perfectly except I can't figure out how to only show the app for certain types of draggable items (URLs, fileURLs, FilePromises and Images). What I have now shows the app for any kind of drag, even selecting text on a page or clicking and clicking and dragging through the menu bar.
I've tried looking at the NSPasteboard for dragging, but that doesn't seem to be updated at drag time. I've seen some posts about using accessibility to see what's under the mouse, but that seems brittle and I'm not yet understanding how to do it.
Here is the code I'm using to detect global drag and drop:
dragMonitor = NSEvent.addGlobalMonitorForEvents(matching:.leftMouseDragged) { event in
if !self.isDragging {
self.isDragging = true
if let dropzoneViewController = self.dropzoneViewController, dropzoneViewController.shouldShowForDrag(event: event) {
self.show()
}
}
}
upMonitor = NSEvent.addGlobalMonitorForEvents(matching:.leftMouseUp) { event in
if self.isDragging {
self.hide()
self.isDragging = false
}
}
That function, in turn, calls the following, which applies the app's logic for determining whether to handle a drag or not.
func shouldShowForDrag(event: NSEvent) -> Bool {
return self.dropTarget.canHandleDrop(NSPasteboard(name: .drag))
}
For clarity's sake, here's how the app handles drags once they are over the app's window:
override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
isReceivingDrag = false
if let dropTarget = dropTarget, dropTarget.canHandleDrop(draggingInfo.draggingPasteboard) {
dropTarget.handleDrop(draggingInfo.draggingPasteboard)
return true
} else {
return false
}
}
The only difference between those two checks is the global check (shouldShowForDrag(event:)) uses NSPasteboard(name: .drag) which is not current at the time NSEvent.addGlobalMonitorForEvents(matching:) fires. The logic when the draggable enters my window uses the provided pasteboard (draggingInfo.draggingPasteboard) which, of course, is accurate to what's being dragged.
Finally, here's the basic logic for determining what drags to accept:
func canHandleDrop(_ pasteBoard: NSPasteboard) -> Bool {
let urlFilteringOptions = [NSPasteboard.ReadingOptionKey.urlReadingContentsConformToTypes:NSImage.imageTypes]
if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options:urlFilteringOptions) as? [URL], urls.count > 0 {
return true
} else if let filePromises = pasteBoard.readObjects(forClasses: [NSFilePromiseReceiver.self], options: nil) as? [NSFilePromiseReceiver], filePromises.count > 0 {
return true
} else if let images = pasteBoard.readObjects(forClasses: [NSImage.self], options: [:]) as? [NSImage], images.count > 0 {
return true
}
return false
}
The first two clauses are the most important. Detecting NSImages is not strictly required.
I know it can be done because I'm using other apps (to do similar, but different, things), and they work exactly like I'm trying to achieve. But so far I'm banging my head against the wall.
Thanks
I'm creating an app for Mac OSX, (I don't know anything about SwiftUI/Cocoa) this app needs to be hidden when the user presses one button and at a certain time the app should pop-up again.
It's a reminder app:
#IBAction func hidMe(_ sender: NSButton) {
window.close() //I know this will remove the window, but I didn't find another way...
}
//I created this function that was supposed to bring the window to front, but I have no idea how to call it.
func reopen() {
let now = Date()
let components = Calendar.current.dateComponents([.month, .day, .hour, .minute], from: now)
if ((components.hour == 9) && (components.minute == 16)) {
window?.makeKeyAndOrderFront(self)
}
I have a NSWindow, on which i apply this:
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.titleVisibility = NSWindowTitleVisibility.Hidden;
window.titlebarAppearsTransparent = true;
I then add a NSView behind the titlebar to simulate a bigger one.
Now it looks like this:
I want to be able to move the window, by dragging the light-blue view. I have already tried to subclass NSView and always returning true for mouseDownCanMoveWindow using this code:
class LSViewD: NSView {
override var mouseDownCanMoveWindow:Bool {
get {
return true
}
}
}
This didn't work.
After some googling i found this INAppStoreWindow on GitHub. However it doesn't support OS X versions over 10.9, so it's completely useless for me.
Edit1
This is how it looks in the Interface Builder.
How can i move the window, by dragging on this NSView?
None of the answers here worked for me. They all either don't work at all, or make the whole window draggable (note that OP is not asking for this).
Here's how to actually achieve this:
To make a NSView control the window with it's drag events, simply subclass it and override the mouseDown as such:
class WindowDragView: NSView {
override public func mouseDown(with event: NSEvent) {
window?.performDrag(with: event)
}
}
That's it. The mouseDown function will transfer further event tracking to it's parent window.
No need for window masks, isMovableByWindowBackground or mouseDownCanMoveWindow.
Try setting the window's movableByWindowBackground property to true.
There are two ways to do this. The first one would be to set the NSTexturedBackgroundWindowMask as well as the windows background color to the one of your view. This should work.
Otherwise you can take a look at this Sample Code
I somehow managed to solve my problem, i don't really know how, but here are some screenshots.
In the AppDelegate file where i edit the properties of my window, i added an IBOutlet of my contentView. This IBOutlet is a subclass of NSView, in which i've overriden the variable mouseDownCanMoveWindow so it always returns false.
I tried this before in only one file, but it didn't work. This however solved the problem.
Thanks to Ken Thomases and Max for leading me into the right direction.
Swift3.0 Version
override func viewDidAppear() {
//for hide the TitleBar
self.view.window?.styleMask = .borderless
self.view.window?.titlebarAppearsTransparent = true
self.view.window?.titleVisibility = .hidden
//for Window movable with NSView
self.view.window?.isMovableByWindowBackground = true
}
Swift 3:
I needed this but dynamically. It's a little long but well worth it (IMHO).
So I decided to enable this only while the command key is down. This is achieved by registering a local key handler in the delegate:
// MARK:- Local key monitor
var localKeyDownMonitor : Any? = nil
var commandKeyDown : Bool = false {
didSet {
let notif = Notification(name: Notification.Name(rawValue: "commandKeyDown"),
object: NSNumber(booleanLiteral: commandKeyDown))
NotificationCenter.default.post(notif)
}
}
func keyDownMonitor(event: NSEvent) -> Bool {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command]:
self.commandKeyDown = true
return true
default:
self.commandKeyDown = false
return false
}
}
which is enabled within the delegate startup:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Watch local keys for window movenment, etc.
localKeyDownMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> NSEvent? in
return self.keyDownMonitor(event: event) ? nil : event
}
}
and its removal
func applicationWillTerminate(_ aNotification: Notification) {
// Forget key down monitoring
NSEvent.removeMonitor(localKeyDownMonitor!)
}
Note that when the commandKeyDown value is changed by the key down handler. This value change is caught by the didset{} to post a notification. This notification is registered by any view you wish to have its window so moved - i.e., in the view delegate
override func viewDidLoad() {
super.viewDidLoad()
// Watch command key changes
NotificationCenter.default.addObserver(
self,
selector: #selector(ViewController.commandKeyDown(_:)),
name: NSNotification.Name(rawValue: "commandKeyDown"),
object: nil)
}
and discarded when the viewWillDisappear() (delegate) or the window controller windowShouldClose(); add this
<your-view>.removeObserver(self, forKeyPath: "commandKeyDown")
So sequence goes like this:
key pressed/release
handler called
notification posted
The view's window isMovableByWindowBackground property is changed by notification - placed within view controller / delegate or where you registered the observer.
internal func commandKeyDown(_ notification : Notification) {
let commandKeyDown : NSNumber = notification.object as! NSNumber
if let window = self.view.window {
window.isMovableByWindowBackground = commandKeyDown.boolValue
Swift.print(String(format: "command %#", commandKeyDown.boolValue ? "v" : "^"))
}
}
Remove the tracer output when happy. See it in action in SimpleViewer on github.
I have an app which must run all the time (if the user agree whit this).
When the user quit the app, I transform the foreground app into a LSUIElement (the app only has a menu bar icon, the dock icon and the menu disappear).
I have an options in the menu item which works ok and transform the LSUIElement into a foreground app (I use the functions [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular] and [NSApp activateIgnoringOtherApps:YES]).
My problem appear when the user double click on the app. I use again the [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular] in the delegate method applicationWillUnhide:(NSNotification *)notification, and all works well except the menu which doesn't appear. If I go to another app, and then I came back the menu appear. I try different methods but I wasn't able to find a good one.
I want to know is a delegate method which is called when the user double clicks on the app, or what is the function from NSApplication which is called in that moment, because I think using the setActivationPolicy: in the applicationWillUnhide function is to late.
To transform a normal application to a LSUIElement I use
ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToUIElementApplication);
And to change it back to foreground :
ProcessSerialNumber psn = { 0, kCurrentProcess };
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
Here is the answer. I have already done the hide/show before I find this question. And this question inspired me to the final answer.
Here what the below code does:
when app starts, app shows in dock and a menubar item shows.
when user clicks the menubar item, app hides and remove from dock.
when user clicks again, the app shows back to dock.
if the app is hidden and user opens the app again from double click or launchpad, the app shows again in dock.
if the app is not hidden but obscured by other apps, clicking the menubar item or relaunching it will put the app to the front.
when a user click the close button on the window, the app removed from dock.
when a user quit the app by cmd+q or from file menu, the app quits and the menubar item quits as well.
I have removed other code that is not directly related.
Other things you may notice:
LSUIElement is not set or set to NO for my Info.plist. If you want to set to yes. You need to set no initiate view controller in storyboard and construct from the window controller yourself.
You will also to deal the logic from left mouse click on menubar item, as you doesn't has the window from the very beginning.
Codes:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
private let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
weak private var window:NSWindow? = nil
func applicationDidFinishLaunching(_ aNotification: Notification) {
setupMenubarTray()
self.window = NSApp.orderedWindows.first
NotificationCenter.default.addObserver(self, selector: #selector(windowWillClose(_:)), name: NSWindow.willCloseNotification, object: self.window!)
}
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !window!.isVisible {
activeApp()
return false
}
return true
}
}
extension AppDelegate {
#objc func windowWillClose(_ noti:Notification) {
removeFromDock()
}
private func showInDock() {
NSApp.setActivationPolicy(.regular)
}
private func removeFromDock() {
NSApp.setActivationPolicy(.accessory)
}
}
// MARK: - setup menubar button
extension AppDelegate {
private func setupMenubarTray() {
guard let button = statusItem.button else {
fatalError()
}
setTrayIcon(for:button)
button.action = #selector(mouseLeftButtonClicked)
}
private func setTrayIcon(for button:NSStatusBarButton) {
let useMonochromeIcon = UserDefaults.standard.bool(forKey: DefaultsKey.useMonochromeIcon.key)
button.image = NSImage(imageLiteralResourceName: useMonochromeIcon ? "MonochromeIcon" : "TrayIcon")
}
#objc private func mouseLeftButtonClicked() {
if NSApp.isHidden || !window!.isKeyWindow {
self.activeApp()
} else {
self.hide()
}
}
private func activeApp() {
showInDock()
window?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
checker.sendNotification()
}
private func hide() {
removeFromDock()
NSApp.hide(nil)
}
}