Adding Click Handler to NSTextField in Swift - macos

I am having trouble detecting a user's double click in swift, I want to detect when they double click on an NSTextField.
func someFunc() {
y.target = self
y.action = "editLabel:"
}
#IBAction func editLabel(obj:AnyObject?) {
NSLog("here");
}
The above code doesn't work, I can't seem to find the basic documentation that shows how to add event handlers. Is there a simpler way to do this?

I guess your text field is a label, not an editable text field in its normal state. Starting with OS X 10.10 (Yosemite), you can use NSClickGestureRecognizer:
func applicationDidFinishLaunching(aNotification: NSNotification) {
let gesture = NSClickGestureRecognizer()
gesture.buttonMask = 0x1 // left mouse
gesture.numberOfClicksRequired = 2
gesture.target = self
gesture.action = "editLabel:"
myLabel.addGestureRecognizer(gesture)
}
func editLabel(sender: NSGestureRecognizer) {
if let label = sender.view as? NSTextField {
print("Hello world")
}
}

A text field does not handling editing, as such. When a text field has focus, a text view is added to the window, overlapping the area of the text field. This is called the "field editor" and it is responsible for handling editing.
It seems the most likely place for you to change the behavior of a double-click is in the text storage object used by that text view. NSTextStorage inherits from NSMutableAttributedString which inherits from NSAttributedString which has a -doubleClickAtIndex: method. That method returns the range of the text that should be selected by a double-click at a particular index.
So, you'll want to implement a subclass of NSTextStorage that overrides that method and returns a different result in some circumstances. NSTextStorage is a semi-abstract base class of a class cluster. Subclassing it requires a bit more than usual. You have to implement the primitive methods of NSAttributedString and NSMutableAttributedString. See the docs about it.
There are a few places to customize the field editor by replacing its text storage object with an instance of your class:
You could implement a custom subclass of NSTextFieldCell. Set your text field to use this as its cell. In your subclass, override -fieldEditorForView:. In your override, instantiate an NSTextView. Obtain its layoutManager and call -replaceTextStorage: on that, passing it an instance of your custom text storage class. (This is easier than putting together the hierarchy of objects that is involved with text editing, although you could do that yourself.) Set the fieldEditor property of the text view to true and return it.
In your window delegate, implement -windowWillReturnFieldEditor:toObject:. Create, configure, and return an NSTextView using your custom text storage, as above.

Related

How to align a toolbar (or its items) with the leading edge of a split view controller's child?

In iOS, a toolbar can be added to any view. In macOS however, it seems only possible to add a toolbar to a window.
I'm working on an app with a split view controller with a toolbar but the toolbar's items only have a meaning with respect to the right view controller's context.
E.g. let's say I have a text editor of some sort, where the left pane shows all documents (like in the Notes app) and the right pane shows the actual text which can be edited. The formatting buttons only affect the text in the right pane. Thus, it seems very intuitive to place the toolbar within that right pane instead of stretching it over the full width of the window.
Is there some way to achieve this?
(Or is there a good UX reason why this would be a bad practice?)
I've noticed how Apple solved this problem in terms of UX in their Notes app: They still use a full-width toolbar but align the button items that are only related to the right pane with the leading edge of that pane.
So in case, there is no way to place a toolbar in a view controller, how can I align the toolbar items with the leading edge of the right view controller as seen in the screenshot above?
Edit:
According to TimTwoToes' answer and the posts linked by Willeke in the comments, it seems to be possible to use Auto Layout for constraining a toolbar item with the split view's child view. This solution would work if there was a fixed toolbar layout. However, Apple encourages (for a good reason) to let users customize your app's toolbar.
Thus, I cannot add constraints to a fixed item in the toolbar. Instead, a viable solution seems to be to use a leading flexible space and adjust its size accordingly.
Initial Notes
It turns out this is tricky because there are many things that need to be considered:
Auto Layout doesn't seem to work properly with toolbar items. (I've read a few posts mentioning that Apple has classified this as a bug.)
Normally, the user can customize your app's toolbar (add and remove items). We should not deprive the user of that option.
Thus, simply constraining a particular toolbar item with the split view or a layout guide is not an option (because the item might be at a different position than expected or not there at all).
After hours of "hacking", I've finally found a reliable way to achieve the desired behavior that doesn't use any internal / undocumented methods. Here's how it looks:
How To
Instead of a standard NSToolbarFlexibleSpaceItem create an NSToolbarItem with a custom view. This will serve as your flexible, resizing space. You can do that in code or in Interface Builder:
Create outlets/properties for your toolbar and your flexible space (inside the respective NSWindowController):
#IBOutlet weak var toolbar: NSToolbar!
#IBOutlet weak var tabSpace: NSToolbarItem!
Create a method inside the same window controller that adjusts the space width:
private func adjustTabSpaceWidth() {
for item in toolbar.items {
if item == tabSpace {
guard
let origin = item.view?.frame.origin,
let originInWindowCoordinates = item.view?.convert(origin, to: nil),
let leftPane = splitViewController?.splitViewItems.first?.viewController.view
else {
return
}
let leftPaneWidth = leftPane.frame.size.width
let tabWidth = max(leftPaneWidth - originInWindowCoordinates.x, MainWindowController.minTabSpaceWidth)
item.set(width: tabWidth)
}
}
}
Define the set(width:) method in an extension on NSToolbarItem as follows:
private extension NSToolbarItem {
func set(width: CGFloat) {
minSize = .init(width: width, height: minSize.height)
maxSize = .init(width: width, height: maxSize.height)
}
}
Make your window controller conform to NSSplitViewDelegate and assign it to your split view's delegate property.1 Implement the following NSSplitViewDelegate protocol method in your window controller:
override func splitViewDidResizeSubviews(_ notification: Notification) {
adjustTabSpaceWidth()
}
This will yield the desired resizing behavior. (The user will still be able to remove the space completely or reposition it, but he can always add it back to the front.)
1 Note:
If you're using an NSSplitViewController, the system automatically assigns that controller to its split view's delegate property and you cannot change that. As a consequence, you need to subclass NSSplitViewController, override its splitViewDidResizeSubviews() method and notify the window controller from there. Your can achieve that with the following code:
protocol SplitViewControllerDelegate: class {
func splitViewControllerDidResize(_ splitViewController: SplitViewController)
}
class SplitViewController: NSSplitViewController {
weak var delegate: SplitViewControllerDelegate?
override func splitViewDidResizeSubviews(_ notification: Notification) {
delegate?.splitViewControllerDidResize(self)
}
}
Don't forget to assign your window controller as the split view controller's delegate:
override func windowDidLoad() {
super.windowDidLoad()
splitViewController?.delegate = self
}
and to implement the respective delegate method:
extension MainWindowController: SplitViewControllerDelegate {
func splitViewControllerDidResize(_ splitViewController: SplitViewController) {
adjustTabSpaceWidth()
}
}
There is no native way to achieve a "local" toolbar. You would have to create the control yourself, but I believe it would be simpel to make.
Aligning the toolbar items using autolayout is described here. Align with custom toolbar item described by Mischa.
The macOS way is to use the Toolbar solution and make them context sensitive. In this instance the text attribute buttons would enable when the right pane has the focus and disable when it looses the focus.

UI Save/Restoration mechanism in Cocoa via Swift

I'd like to save the state of Check Box, quit application, then launch macOS app again to see restored state of my Check Box. But there's no restored state in UI of my app.
What am I doing wrong?
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var tick: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(tick.state, forKey: "")
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let state = coder.decodeObject(forKey: "") as? NSControl.StateValue {
tick.state = state
}
}
}
To the best of my knowledge, this is the absolute minimum you need to implement custom UI state restoration of a window and/or its contents.
In this example, I have a window with a checkbox and that checkbox's state represents some custom view state that I want to restore when the app is relaunched.
The project contains a single window with a single checkbox button. The button's value is bound to the myState property of the window's content view controller. So, technically, the fact that this is a checkbox control is irrelevant; we're actually going to preserve and restore the myState property (the UI takes care of itself).
To make this work, the window's restorable property is set to true (in the window object inspector) and the window is assigned an identifier ("PersistentWindow"). NSWindow is subclassed (PersistentWindow) and the subclass implements the restorableStateKeyPaths property. This property lists the custom properties to be preserved/restored.
Note: if you can define your UI state restoration in terms of a list of key-value compliant property paths, that is (by far) the simplest solution. If not, you must implement encodeRestorableState / restoreState and are responsible for calling invalidateRestorableState.
Here's the custom window class:
class PersistentWindow: NSWindow {
// Custom subclass of window the perserves/restores UI state
// The simple way to preserve and restore state information is to just declare the key-value paths
// of the properties you want preserved/restored; Cocoa does the rest
override class var restorableStateKeyPaths: [String] {
return [ "self.contentViewController.myState" ]
}
// Alternatively, if you have complex UI state, you can implement these methods
// override func encodeRestorableState(with coder: NSCoder) {
// // optional method to encode special/complex view state here
// }
//
// override func restoreState(with coder: NSCoder) {
// // companion method to decode special/complex view state
// }
}
And here's the (relevant portion) of the content view controller
class ViewController: NSViewController {
#objc var myState : Bool = false
blah, blah, blah
}
(I built this as a Cocoa app project, which I could upload if someone tells me where I could upload it to.)
Actually you don't have to go through restorableStateKeyPaths / KVO / KVC if you don't want to.
I was stuck in the same state as you with the encodeRestorableState() & restoreState() methods not being called but found out what was missing.
In System Preferences > General, make sure "Close windows when quitting an app" is unchecked.
Make sure that the NSWindow containing your view has "Restorable" behavior enabled in IB.
Make sure that your NSViewController has a "Restoration ID" set.
Your NSViewController won't be encoded unless you call invalidateRestorableState(). You need to call this each time there's a state in your NSViewController that changes and that you want to have saved.
When no state changes in the NSViewController after having restored it, its state would not be encoded again when closing the app. Which would cause the custom states to not be restored when relaunching the app. The simplest way I found is to also call invalidateRestorableState() in viewDidLoad(), so that state is always saved.
After doing all that, I didn't even have to additionally implement NSApplicationDelegate or NSWindowRestoration protocol methods. So the state restoration of the NSViewController is pretty self-contained. Only external property is restorable NSWindow.
After losing a couple of hours of my life to this problem I finally got it working. Some of the information in the other answers was helpful, some was missing, some was not necessary.
Here is my minimal example based on a new Xcode 13 project:
in AppDelegate add (this is missing in the other examples):
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true }
in ViewController add:
#objc var myState : Bool = false
override class var restorableStateKeyPaths: [String] {
return [ "myState" ]
}
set up some UI and bind it to myState to see what is going on
make sure System Preferences > General > "Close windows when quitting an app" is unchecked
Things that I did not need to do:
create a custom window subclass
set a custom restoration id
it worked fine just with Xcode start/stop

Custom NSTextFieldCell draws text differently

I have view-based NSTableView with 2 text columns.
First column uses custom NSTextFieldCell. Second column uses the default cell.
Here's the custom cell code:
class CustomTextFieldCell: NSTextFieldCell {
// don't do anything, just call the super implementation
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) {
super.drawInterior(withFrame: cellFrame, in: controlView)
}
}
Somehow the text fields in the custom (first) column look differently (thinner?).
Here's the screenshot (Sierra).
Not sure what's going on, may be custom implementation uses different antialiasing settings.
What am I missing there? Thank you in advance.
edit: mentioned view-based NSTableView
Looks like that bug still exists today. Overriding NSTextField.draw(_ dirtyRect: NSRect) or NSTextFieldCell.draw(withFrame cellFrame: NSRect, in controlView: NSView) and simply calling super is enough to reproduce this behavior.
As a workaround, set textField.drawsBackground = false, place your text field into an NSBox or custom NSView and handle your drawing there.

How to let multiple components/controls share similar action in Xcode

I'm using Xcode 6 and Swift to develop an OS X app, not iOS.
Let's say we have two toggle buttons and each one controls a combobox. Everytime press the button, it will enable or disable the combobox it controls. I can definately set up separate actions for each button. Since I have ten buttons, this approach seems to contain a lot of redundant code.
#IBAction func clickBtn1 (sender: NSButton){
if combobox1.enabled == true
{
combobox1.enabled = faulse;
}
else
{
combobox1.enabled = true;
}
}
#IBAction func clickBtn2 (sender: NSButton){
//same codes for combobox 2
}
Is there any way to make this simpler, such as share the action code by identify different sender, Similar to VB.NET?
UPDATE:
I found a imcomplete solution for it from https://stackoverflow.com/a/24842728/2784097
now I control+drag the two buttons to the same action in ViewController.swift and also give those two buttons different tag. button1.tag=1, button2.tag = 2. The code now looks like,
//button1.tag=1, button2.tag = 2.
#IBAction func clickButton(sender:NSButton) {
switch(sender.tag){
case 0:
combobox1.enabled = !combobox1.enabled;
break;
case 1:
combobox2.enabled = !combobox2.enabled;
break;
default:
break;
}
}
This solves a part of my problem. Next, I wonder is there any way to access/find the controls/components by reference, for example a string or tag or name anything. Pseudo code would like following,
//button1.tag=1, button2.tag = 2.
#IBAction func clickButton(sender:NSButton) {
//pseudo code
combobox[button.tag].enabled = !combobox[button.tag].enabled;
}
You should be able to bind all of the NSButton (and NSComboBox) control events to the same buttonClicked: method. You can achieve this through Interface Builder or programmatically via the setAction: method.
Selectors in Swift are just strings, like "buttonClicked:", but I prefer to wrap them in Selector initializers for clarity (e.g. Selector("buttonClicked:")).
There are many ways to do this, here's one:
Extend the NSButton class to include a property for the combo box that it controls. That way, when in the action method, you can get a reference to the correct combo box from the instance of the button that is passed in.
Create a new class MyButton that extends NSButton
Add a public property to that class to hold an reference to a combo box.
Replace your NSButtons with MyButtons.
After the view loads, set the combo box property on each of your buttons to the correct combo box.
Write a new action method that accepts a MyButton object as the sender. Get the combo box property from the sender and call combobox.enabled = !combobox.enabled.

NSBorderlessWindowMask Subview NSTextfield not keybard editable Swift

I'm using a NSBorderlessWindowMask for my main window on a Swift project (without storyboards), when I load a Subview, the NSTextfield outlet is not keybard editable. I already put this code on the initialisation:
self.window?.makeKeyWindow()
self.window?.becomeKeyWindow()
this allows the outlet to be "blue" like on focus, but the keyboard editing is disabled, i can copy/paste on the textfield
You need to use a custom subclass of NSWindow and override canBecomeKeyWindow() to return true. By default, it returns false for windows without title bars (as documented).
You probably want to do the same for canBecomeMainWindow().
Also, never call becomeKeyWindow() (except to call super in an override). That is called by Cocoa to inform the window that it has become the key window. It does not instruct the window to become the key window.
I found an awesome workaround for this problem:
basically setup at beginning the NSWindow mask as NSTitledWindowMask, when application is loaded, remove set up the new mask NSBorderlessWindowMask
func applicationWillFinishLaunching(notification: NSNotification) {
self.window?.titleVisibility = NSWindowTitleVisibility.Hidden
self.window?.styleMask = NSTitledWindowMask // adds title bar
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
self.window?.makeKeyWindow()
self.window?.becomeKeyWindow()
self.window.setIsVisible(true)
self.window?.styleMask = NSBorderlessWindowMask // removes title bar
}

Resources