How can I read and get notified a computed property of a class in cocoa macos? - swift

In NSWindow class there is a computed property called isVisible, I am creating a custom NSWindow and i am calling it MyNSWindow so I want look the value change of isVisible for MyNSWindow, for example the value should be false before window get available in screen, and after that the value should be true, so I want observe this value change, how can i do this? Also I am not looking for an answer with Combine.
This is my code:
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
private var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
window = MyNSWindow(
contentRect: NSRect(x: 0, y: 0, width: 100.0, height: 100.0),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.setFrameAutosaveName("Main Window")
window.title = "No Storyboard Window"
window.makeKeyAndOrderFront(window)
window.center()
}
}
class MyNSWindow: NSWindow {
}

Apple has made it difficult to reliably be notified when a window becomes visible or hidden. I've done some investigating for my own project, and here's what I've discovered:
Apple's recommended solution is to set an NSViewController as the window's contentViewController. Override the view controller's viewWillAppear or viewDidAppear method to be notified when the window becomes visible. Override the view controller's viewWillDisappear or viewDidDisappear method to be notified when the window becomes hidden.
I know this is Apple's recommend solution because I opened a feedback (in 2016) requesting a public windowWillShow notification and was told:
Our more modern way of doing this is to use Storyboards with a View Controller; the contentViewController will get a viewWillAppear — this is the hook you should be using now.
As far as I know from very limited testing, this method is reliable, so it may work well for you. I have an existing app with many windows that were implemented without contentViewControllers that I do not wish to retrofit, so this doesn't work well for me. Also, you can only apply this to windows you create—not, for example, to the window created by NSAlert.
visible is not KVO-compliant.
There are no documented notifications posted when visible changes from false to true.
The NSWindow.willCloseNotification is supposed to be posted when visible changes from true to false, but it doesn't get posted when a window presented as a sheet is dismissed.
It should be possible to use Cocoa Bindings to bind to visible (using the NSVisibleBinding constant), but the binding is not updated when a window presented as a sheet is dismissed. It is also annoying and obscure to implement.
There is no single, public “funnel” method you can override on NSWindow to be notified. The orderOut(_:), orderFront(_:), and orderBack(_:) methods do go through order(_:relativeTo:), but orderFrontRegardless() does not. Sheet presentation and dismissal do go through order(_:relativeTo:), so if you never use orderFrontRegardless(), making an NSWindow that overrides order(_:relativeTo:) may be a good solution for you. You will not, however, be notified of the appearance and disappearance of windows created by AppKit for you, such as the window created by NSAlert. If you create any NSPanels, you'll also want to create an NSPanel subclass.
AppKit posts a number of undocumented notifications when windows change visibility, that are posted reliably even for sheet presentation and dismissal. Two of these are named _NSWindowWillBecomeVisible and NSWindowWillOrderOffScreenNotification. Use at your own risk.

Related

Prevent main AppKit app window from opening when loading a SwiftUI preview

I have a Mac App that's mostly written in AppKit. Over time, I'm adding new views in SwiftUI, and all the while I've been using SwiftUI previews to preview both my new SwiftUI views, and my old AppKit views/view controllers (see Previews are not just for SwiftUI views).
One nuisance I've been running into is that starting a SwiftUI preview opens my app's main window. This is a consequence of how previews go through the regular startup flow, e.g. calling applicationDidFinishLaunching(_:) on my App Delegate, which is where I instantiate and present my app's main window on start. Naturally, I don't want that to happen if I'm working on a view in a preview.
Here's what my AppDelegate looks like:
#NSApplicationMain
public class AppDelegate: NSObject, NSApplicationDelegate {
lazy var mainWindowController = MainWindowController()
public func applicationDidFinishLaunching(_ aNotification: Notification) {
// Nasty hack :(
let isInSwiftUIPreview = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
if !isRunningInTest() && !isInSwiftUIPreview {
mainWindowController.showWindow(self)
}
}
// ...
}
As you can see, I've added special cases for not showing the main window if this code is running in a test or SwiftUI preview. This smells, and suggests to me that I'm doing it wrong.
If fact, Apple mentions this exact issue in Mastering Xcode Previews, at 41:41. They suggest to make your app scene-aware and use a UISceneDelegate to do this work instead. However, that's an iOS/catalyst-only API, and I don't see an obvious alternative for AppKit.
With all that said: What's the right way to open the main window of an AppKit app, so that it doesn't open during tests or SwiftUI previews?

Prevent VoiceOver From Reading UIButton as "Possible Text"

I am writing a very simple game that is designed specifically for blind users, but may also be used by sighted users. It uses many buttons as elements, however, blind users interact with these buttons through custom gestures (pan, tap, etc), so standard voiceover interaction is not appropriate.
The issue lies in the fact that there are no accessibility objects on the screen at all, so whenever the game loads, voiceover starts reading the labels on buttons (e.g. "Possible text: back, menu...). These buttons are read regardless of the fact that they are not enabled. I also can't remove most of them from the view for blind users.
I have tried turning off accessibility for the elements, unchecking "button" from accessibility traits, everything has allows direct interaction selected, I have tried .accessibilityElementsHidden, all the suggestions from How do you exclude a UIButton from VoiceOver? and nothing seems to work.
My current solution has a clear UILabel with no text in it, this is set to the only item in the .accessibilityElements array, and then for good measure I post an accessibility screen changed notification with that label as the object so it becomes focused, then I wait a second in a dispatch queue async after call, remove the label entirely, and set focus back to the main view so the user can interact.
Here is an example of my current solution:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.accessibilityElements = [lblVoiceOver!]
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIAccessibility.post(notification: .screenChanged, argument: lblVoiceOver)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.lblVoiceOver.removeFromSuperview()
UIAccessibility.post(notification: .screenChanged, argument: self.view)
}
}
This is a silly hack, at best, and I would love to implement a real solution that simply prevents the "Possible text" from being read by voiceover. I believe the possible text feature was added in iOS 11, to help apps that are not written with accessibility in mind to be more accessibility friendly, but so far I haven't found a way to turn this off.
The issue lies in the fact that there are no accessibility objects on the screen at all.
If you want to reach this purpose, just write self.view.accessibilityElementsHidden = true in your view controller that will contain no accessible element for VoiceOver anymore: this will indicate that the content of your container isn't accessible.
blind users interact with these buttons through custom gestures (pan, tap, etc), so standard voiceover interaction is not appropriate [...] I would love to implement a real solution that simply prevents the "Possible text" from being read by voiceover.
... following the preceding rationale, you should prevent VoiceOver from analyzing and reading anything in your view.
Now, dealing just with your buttons, I created a blank project with a simple code for the view controller hereafter (Swift 5.0, iOS 12):
import UIKit
class NonAccessibleButtonVC: UIViewController {
#IBOutlet weak var aboveLabel: UILabel!
#IBOutlet weak var belowLabel: UILabel!
#IBOutlet weak var myButton: UIButton!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myButton.accessibilityElementsHidden = true
}
}
... and you get the following result on your device:
The button isn't taken into account as an accessible element and if it doesn't work in your code, it means that anything else prevents this correct operation. Add maybe the setAccessibleElement button property value to false according to your context ?
However, another solution could be defining the display of the desired accessibility objects in your view thanks to its accessibilityElements property taking away the buttons for instance (see Example 2 of this link): that will definitely work in addition to ordering all your elements.

Close NSWindow but do not nil it

I have a window-less app with the "Application is agent" set to YES. In that app I have a "let mainWindow: NSWindow" and with the click on a menu item I do the following:
func createNewWindow(){
mainWindow.center()
mainWindow.hidesOnDeactivate = true
mainWindow.isMovableByWindowBackground = true
mainWindow.backgroundColor = NSColor(calibratedHue: 0, saturation: 0, brightness: 1, alpha: 0.4)
mainWindow.contentViewController = searchView
mainWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
If I click outside my window or in any way shift focus to another app, my window closes and I have to click my menu item to open it again. Now this is what I want, so this is great. But if I in the view controller call "self.view.window?.close()" and there after click my menu item to open my window again, I get a bad access when accessing mainWindow, so I am guessing something has set it to nil when I close it.
How do I close my window in the same way as when I click outside the window? Or how do I otherwise prevent the bad access error?
Thank you
Søren
Not something, you set it to nil when you close the window.
From the documentation of close()
If the window is set to be released when closed, a release message is
sent to the object after the current event is completed. For an
NSWindow object, the default is to be released on closing, while for
an NSPanel object, the default is not to be released. You can use the
isReleasedWhenClosed property to change the default behavior.
Either change the behavior or call orderOut(_:) which doesn't release the window.

Use NSPanel for user input. Not opening up again

I want to display a 'NSPanel' for the user to input a name for a new folder. Why a NSPanel? Because it looks awesome! It hosts one TextField and one PushButton to confirm the name. It shall also close the window when clicked.
It displays when the "add" button gets clicked in my menu. It also closes when the "done" button gets clicked in the NSPanel. But when I click "add" again it doesn't show up anymore. That also occurs when I close it via the normal "close button" in the title bar. So it is not explicitly related to the "done"-PushButton. I also tested implementing func windowWillClose(notification: NSNotification) which also doesn't get triggered in either cases. What could be the problem? Also, does it somehow need to be a "new" window every time? Or am I using this correctly for user input? I mean it just gets instantiated once and then "shown" and "unshown" or am I wrong?
So I did a new Cocoa-Class - Subclass of NSWindowController - and let xCode create a .xib for that also. In that .xib I "designed" the NSPanel. I ticked visible at launch without that the window wouldn't appear when the menu button gets clicked. I also hooked up an IBOutlet for the NSPanelin my Cocoa Class. My Class at the moment looks like this:
import Cocoa
class NamingHUD: NSWindowController, NSWindowDelegate {
#IBOutlet var insertNameWindow: NSPanel!
#IBOutlet weak var nameTextField: NSTextField!
override var windowNibName : String! {
return "NamingHUD"
}
override func windowDidLoad() {
super.windowDidLoad()
insertNameWindow.center()
insertNameWindow.makeKeyAndOrderFront(nil)
NSApp.activateIgnoringOtherApps(true)
}
#IBAction func userSetName(sender: NSButton) {
print("Close button clicked")
insertNameWindow.close()
}
}
In my Main Class I declared it as a variable like this:
var namingHUD:NamingHUD!
and then in override func awakeFromNib() as:
namingHUD = NamingHUD()
as well as in a click handler like:
#IBAction func addClicked(sender: NSMenuItem) {
namingHUD.showWindow(nil)
}
Now. When I click and addClicked() gets called the window shows up as expected. Fine! I enter a name and hit the "done" button and it closes the window properly. Also Fine! But when I click again, say to add another folder, the window doesn't show up anymore. I also created a Preferences Window the exact same way. But with a Window instead of a NSPanel inside. That totally works as it should.
So I clearly confused something or forget something. What could it be? I openly admit that it is the first time I am working with any kind of window outside of following a tutorial. So I clearly didn't grasp the whole concept of it. I read up about windows in Apples Developer Guide and it kinda makes sense. But... well, doesn't work at the moment. Am I "misusing" the NSPanel? Shouldn't be the case as it inherits from NSWindow or?
Did you connect the window outlet of NamingHUD to your awesome panel? Nibs are loaded lazily:
namingHUD = NamingHUD() // init the controller but doesn't load the nib
...
namingHUD.showWindow(nil) // now you are loading it for the first time
It works the first time because showWindow() loads the nib and show the window referenced by the window outlet. Your panel shows up because it's set to "Visible at launch". Your of course had no window to show.
Subsequent clicks don't load the nib file again, only order the window outlet to show up. That's why your panel did not show again. FYI: an NSPanel is a subclass of NSWindow so it has everything that NSWindow has, and then some more.

Keep window always on top?

In Objective-C for Cocoa Apps it's possible to use such way to keep window always on top?
How to achieve the same with Swift?
self.view.window?.level = NSFloatingWindowLevel
Causes build error Use of unresolved identifier 'NSFloatingWindowLevel'
To change the window level you can't do it inside viewDidload because view's window property will always be nil there but it can be done overriding viewDidAppear method or any other method that runs after view is installed in a window (do not use it inside viewDidLoad):
Swift 4 or later
override func viewDidAppear() {
super.viewDidAppear()
view.window?.level = .floating
}
For older Swift syntax check the edit history
I would prefer this way. This ignores all other active apps, and makes your app upfront.
override func viewWillAppear() {
NSApplication.sharedApplication().activateIgnoringOtherApps(true)
}
While the other answers are technically correct - when your app will or did resigns active, setting the window level to .floating will have no effect.
.floating means on top of all the windows from the app you are working on, it means not on top of all apps windows.
Yes there are other levels available you could set, like kCGDesktopWindowLevel which you can and should not set in swift to make your window float above all.
None of them will change the fact that your window will go behind the focused and active apps window. To circumvent i chose to observe if the app resigns active notification and act accordingly.
var observer : Any;
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(
forName: NSApplication.didResignActiveNotification,
object: nil,
queue: OperationQueue.main ) { (note) in
self.view.window?.level = .floating;
// you can also make your users hate you, to take care, don't use them.
//NSApplication.shared.activate(ignoringOtherApps: true)
//self.view.window?.orderFrontRegardless();
}
}
another way could be subclassing NSWindow and override the property .level with an always returning .floating, but the code above is less work and keeps control in the place where you want to set the window floating.
I spent a long time trying to make this work. I then realized there was a simple answer, just worded in a different way. Here it is: Change macOS window level with SwiftUI (to make a floating window)
As explained there:
You can access your windows with NSApplication.shared.windows and set the level for each one.
for window in NSApplication.shared.windows {
window.level = .floating
}
EDIT: you can use other levels, including .screenSaver (highest, I think) and ```.normal`` if you want to return to standard behavior. Source: https://developer.apple.com/documentation/appkit/nswindow/level