Start a main window using swift - swift

I'm kind of new in swift, but I'm 'stuck' in something.
I know C, Java, C++, PHP, etc...
I'm trying to compose an multi-windowed App (Mac OS application using Cocoa), using swift. I can already do it, but my problem is when I close the main window. If I close my main window, when I try to add a child to it, I will get an error or the new window just does not open.
I have this:
class SmallWindow: NSViewController {
#IBOutlet var window: NSWindow! //Window inside the .xib file
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
}
I already tried the above using NSWindowController and NSWindow.
var smallwin = SmallWindow()
smallwin.window.makeKeyAndOrderFront(self)
//already tried with smallwin.window too
I am wondering how to programmatically start a new window. Am I missing some steps?
#IBOutlet var window: NSWindow!
#IBOutlet var smallwin : SmallWindow!
func applicationDidFinishLaunching(aNotification: NSNotification) {
smallwin = SmallWindow()
window.addChildWindow(smallwin.window, ordered: NSWindowOrderingMode.Above)
}

You probably don't need a child window. You just need a window.
To put a new window on the screen, you need to do three things:
Create the window, either programmatically or by loading a nib.
Send an order-in message to the window. Usually you want to send makeKeyAndOrderFront or orderFront. See “Opening and Closing Windows” in the Window Programming Guide.
Keep a strong reference to the window. The system doesn't necessarily keep a strong reference to an on-screen window. If the window is deallocated, it will remove itself from the screen.

Related

How to override Copy and Paste NSMenuItems for one View Controller Swift macOS

I am writing a macOS application with multiple view controllers, using Storyboards.
In my main View Controller I would like to be able to copy and paste data to the NSPasteboard. The data is related to buttons displayed to the user, and the exact data to be copied varies depending on which button has most recently been pressed/selected.
I would like to be able to override the standard behaviour of the Copy and Paste NSMenuItems when my main View Controller is the front most (key) window, but revert to back to standard behaviour when other windows are in the foreground, as they all contain NSTextFields which can be copied/pasted into.
I have done a lot of googling, and overriding this behaviour is not very well documented. I can achieve it globally by adding an IBAction into the App Delegate, which I could use to call a function in whichever View Controller is key, but this doesn't feel like a very elegant solution.
Currently my IBAction in the App Delegate looks like this:
#IBAction func copy(_ sender: Any) {
if let window = NSApplication.shared.keyWindow {
if let splitView = window.contentViewController as? SplitViewController {
if let controlVC = splitView.controlItem.viewController as? ControlViewController {
controlVC.copyAction(self)
}
}
}
}
Am I missing a neater solution?
Thanks,
Dan

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.

What's the correct way to pass an object that will never change to an NSWindowController?

I have an NSWindowController that shows a table and uses another controller as the data source and delegate for an NSTableView. This second controller displays information from an object, which is passed in by the NSWindowController. That controller in turns has the object set as a property by the AppDelegate. It looks like this:
class SomeWindowController: NSWindowController {
var relevantThing: Thing!
var someTableController: SomeTableController!
#IBOutlet weak var someTable: NSTableView!
override func windowDidLoad() {
someTableController = SomeTableController(thing: relevantThing)
someTable.dataSource = someTableController
someTable.delegate = someTableController
}
}
In the AppDelegate I then do something like
func applicationDidFinishLaunching(_ aNotification: Notification) {
relevantThing = Thing()
someWindowController = SomeWindowController()
someWindowController.relevantThing = relevantThing
someWindowController.showWindow(nil)
}
Is this a reasonable approach? I feel like the implicitly unwrapped optionals used in SomeWindowController might be bad form. Also, relevantThing is not allowed to change in my case, so I feel a let would be more correct. Maybe the relevantThing should be made constant and passed in through the initializers? Or would that break the init?(coder: NSCoder) initializer?
I'd greatly appreciate any suggestions, as I'm trying to get a feel for the right way to do things in Swift.
A few things:
Is there any reason your are creating your window controller in code and not loading it from a storyboard/xib?
Generally, a better practice is to put all your 'controller' that relates to a view in a NSViewController and use NSWindowController only for stuff that relates to the window itself (e.g. toolbar, window management, etc).
Similarly to iOS, NSViewController is now integrated into the window/view lifecycle and responder chain. For many windows you don't even need to subclass NSWindowController.
XCode's app project template creates a storyboard with the window, main view and their controllers. This is a good starting point.
NSWindowController has a contentViewController property that is set to the NSViewController of the main content view (when loaded from storyboard). You generally don't need a separate view controller property for your view controller.
I think that usually, you want to minimize modifying your controllers from outside code and make them as independent as possible. This makes them more testable and reusable.
If your Thing instance is global for the entire application (as it appears from your code), you may want to consider adding it as a singleton instance to the Thing class and retrieving it from the NSViewController (e.g in viewDidLoad())
If you put your controllers/views in storyboard, you can connect the table's datasource/delegate there. And if this your main window, it can load and show it automatically when the app starts. But in any case, put your NSViewController/View wiring in the view controller.
If you want to separate logic between your main NSViewController into a more specialized view controller that handles a specific part of your view, you can use NSContainerView in Interface Builder to add additional view controllers to handle specific views.

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