(Swift)How to dectect a "fieldEditor" resignFirstResponder() (Mac/Cocoa) - swift

What I intend to do is validate four NSTextFields, I will try to make sure that all of them are filled correctly, and completely.
textField and alert
As the picture shows: if a user types anything in the 1st NSTextField, types something wrong or leaves it blank in the 2nd NSTextField, press Enter or click outside the boundary, it should show an alert, and try to stop the user from resigning from the 2nd NSTextField, and he should confirm his input in 2nd NSTextField.
What I have tried and the problems I faced:
Use textFieldShouldEndEditing(_:)
When I type something. Wrong, show an alert and return false. It runs well.
But if I type nothing, textFieldShouldEndEditing(_:) will not invoke. (Because it won't be triggered without typing)
func control(_ control: NSControl, textShouldEndEditing fieldEditor: NSText) -> Bool {
if control.stringValue != validInput{
showAlert()
return false
}else{
return true
}
}// plz ignore the delegation
CustomNSTextField & resignFirstResponder()
When the user clicked NSTextField, NSTextField would becomeFirstResponder(), followed by ResignFirstResponder() because the FieldEditor would enter. So it is invalid to monitor resignFirstResponder().
Use textFieldShouldEndEditing(_:)
When I type something wrong or type nothing, press Enter or click outside the boundary, show an alert, but it is difficult to stop the following functions: for example, if I click the button "open", it may show an alert and an openFile dialog in the meantime.
What may solve my problems:
Whether there is a trick that invokes textFieldDidBeginEditing (_:) when NSTextField resignFirstResponder() ?
Whether there is a function that monitors the window's current fieldEditor resignFirstResponder(), and it can return false while I want the user to confirm his input.

I found it NOT as difficult as I thought to write a custom fieldEditor. Follow the tutorial here. I post the essential part of my code here for anyone who is facing similar problems.
class CustomFieldCell: NSTextFieldCell {
static var durationFieldEditor: CustomFieldEditor = {
let customFieldEditor = CustomFieldEditor()
return fieldEditor
}()
override func fieldEditor(for controlView: NSView) -> NSTextView? {
return Self.customFieldEditor
}
}
class CustomFieldEditor: NSTextView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
commonInit()
}
override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
super.init(frame: frameRect, textContainer: container)
commonInit()
}
override func becomeFirstResponder() -> Bool {
// condition
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
// condition
return super.resignFirstResponder()
}
private func commonInit() {
isFieldEditor = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Related

Swift 5 - Mac OS - NSTrackingArea overlapping views

Currently I have a little issue when it comes to buttons(NSButton) which have a tracking area and views(NSView overlay) above these buttons, this is my setup:
Custom button:
class AppButton: NSButton {
override func updateTrackingAreas() {
super.updateTrackingAreas()
let area = NSTrackingArea(
rect: self.bounds,
options: [.mouseEnteredAndExited, .activeAlways],
owner: self,
userInfo: nil
)
self.addTrackingArea(area)
}
override func mouseEntered(with event: NSEvent) {
NSCursor.pointingHand.set()
}
override func mouseExited(with event: NSEvent) {
NSCursor.arrow.set()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
An instance of this button class is used i a very basic NSView.
When I hover over the button, the cursor changes correctly.
When I click the button a new overlay(NSView) is opened above the button...
This is where the problem starts:
When I hover over the overlay where my button is placed, the cursor still changes...
I did not know that a NSTrackingArea is going through all views..
How can i solve this issue?
Can I set any property on the overlay(NSView) to somehow disable the NSTrackingArea on the button?
Thanks!!
You can subclass NSView and add local monitoring for events. Check if the event has occurred over the view and if true return nil. This will avoid propagating the events being monitored. If the event is outside the view frame you can propagate it normally returning the event monitored.
class CustomView: NSView {
override func viewWillMove(toSuperview newSuperview: NSView?) {
super.viewWillMove(toSuperview: newSuperview)
wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
layer?.borderWidth = 1
NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited, .leftMouseDown]) { event in
if self.frame.contains(event.locationInWindow) {
// if cursor is over the view just return nil to do not propagate the events
return nil
}
return event
}
}
}
If you are trying to stop mouse events from a viewController, add this code in viewDidLoad
NSEvent.addLocalMonitorForEvents(matching: [.mouseEntered, .mouseExited, .leftMouseDown]) { event in
if self.view.frame.contains(event.locationInWindow) {
return nil
}
return event
}

how to disable the UI Interaction of a MAC App

we can disable and enable user interaction in ios app using
UIApplication.shared.beginIgnoringInteractionEvents()
UIApplication.shared.endIgnoringInteractionEvents()
how to disable MAC App user interaction? we can disable interaction of a view but not the whole app. is there any solution?
You may disable it using hitTest.
class CAInteractionView: NSView {
var isUSerIntactionEnable:Bool = true
override func hitTest(_ point: NSPoint) -> NSView? {
if(self.isUSerIntactionEnable) {
return super.hitTest(point)
}
return nil
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
}
Call it like
if let cView = self.view as? CAInteractionView {
cView.isUSerIntactionEnable = false
}

Best strategy in swift to detect keyboad input in NSViewController

I want to detect keyboard input in my NSViewController.
The idea is to have several actions performed if the user presses certain keys followed by ENTER/RETURN.
I have checked if keyDown would be a appropriate way. But I would receive an event any time the user has pressed a key.
I also have though on using an NSTextField, set it to hidden and let it have the focus.
But maybe there are other better solution.
Any ideas?
Thanks
I've finally got a solution that I like.
First it has nothing todo with any hidden UI Elements but rather let the viewcontroller detect keyboard input.
var monitor: Any?
var text = ""
override func viewDidLoad() {
super.viewDidLoad()
self.monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown, handler: myKeyDownEvent)
}
override func viewWillDisappear() {
//Clean up in case your ViewController can be closed an reopened
if let monitor = self.monitor {
NSEvent.removeMonitor(monitor)
}
}
// Detect each keyboard event
func myKeyDownEvent(event: NSEvent) -> NSEvent {
// keyCode 36 is for detect RETURN/ENTER
if event.keyCode == 36 {
print(text)
text = ""
} else {
text.append( event.characters! )
}
return event
}

NSButton turns gray when pushed

I have a NSButton with an image. When pushed the whole cell turns gray. How to prevent this?
There are several posts about this topic. But most of them are like 10 years old. The most recent one was here: NSButton background transparent after getting focus
According to this, I tried with this code:
class overviewImageButton: NSButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
convenience init(appearance: NSAppearance) {
self.init(appearance: appearance)
self.appearance = NSAppearance(named: NSAppearanceNameAqua)
}
override func draw(_ dirtyRect: NSRect) {
self.image = NSImage(named: "buttonImage.png")
super.draw(dirtyRect)
NotificationCenter.default.addObserver(forName: windowChanged, object: nil, queue: nil) {
notification in
self.image = NSImage(named: "buttonImage_highlighted.png")
}
}
}
But it doesn´t work. The buttoncelll still turns gray when pushed. Thanks for any help!
A lot of this was already said by both Willeke and I'L'I, so credit goes to them.
What Willeke said:
Never do anything in draw() except drawing. This method can get called as often as the screen refreshes. So currently you are basically trying to add yourself as the observer of the notificationCenter really often.
Here is what you could do:
Write a setup() method and call that from each initialiser. Any one of them is called once for a button instance.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupButton()
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.setupButton()
}
private func setupButton() {
self.image = NSImage(named: "buttonImage.png")
NotificationCenter.default.addObserver(forName: .windowChanged, object: nil, queue: nil) {
notification in
self.image = NSImage(named: "buttonImage_highlighted.png")
}
}
You do not need to add the init(frame:) initialiser here. For storyboards the init(coder:) one is sufficient. I added it anyways, because you might want to initialise the button programmatically. You use the init(frame:) method there usually. If you add convenience methods, make sure to call the setup() method there as well.
What I'L'I said:
To the important stuff:
What I did to suppress the grey background on mouseDown was to simply call isHighlighted = false before calling super.draw(rect).
override func draw(_ dirtyRect: NSRect) {
self.isHighlighted = false
super.draw(dirtyRect)
}
Bonus:
I see you somewhere defined a Notification.Name. You can define all of them in a project global extension to Notification.Name like this:
extension Notification.Name {
static let windowChanged = Notification.Name(rawValue: "WindowChangedNotification")
}
After that you can use them everywhere like system notification names.
NSButton state appearances generally are affected with highlightsBy and showsStateBy. These methods change what happens within the NSButtonCell, which I think you're referring.
↳ https://developer.apple.com/documentation/appkit/nsbuttoncell

NSViewController User Interface State Restoration

Summary:
What's the proper way to save/restore the state of an NSSearchField in my NSViewController using the built-in user interface preservation mechanism of Cocoa?
Details:
I'm working on my first macOS app and I'm having a little trouble with state restoration for my user interface. So far I have it working to the point where the encodeRestorableState(with:) and restoreState(with:) methods are called in my NSViewController subclass.
I have an NSSearchField in my view controller and I want to save/restore the state of the search field including its text, any selection, the cursor position, and whether it is currently in focus or not.
If I use the following code, the text is properly saved and restored:
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(searchField.stringValue, forKey: "searchText")
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let searchText = coder.decodeObject(forKey: "searchText") as? String {
searchField.stringValue = searchText
}
}
Obviously I can add more code to save/restore the search field's selection and cursor position, etc.
My real question is, is there a better, proper, more automatic way to save and restore the search field's state? Or is it required that I write my own code for each attribute of the search field I wish to save?
I tried using:
searchField.encodeRestorableState(with: coder)
and:
searchField.restoreState(with: coder)
in the two above methods but that didn't result in anything appearing in the search field when my app was restarted.
I also implemented:
override class func restorableStateKeyPaths() -> [String] {
var keys = super.restorableStateKeyPaths()
keys.append("searchField.stringValue")
return keys
}
where "searchField" is the name of the NSTextField outlet property in my view controller. This method is called when my app is launched but the text was not restored in the search field.
This view controller is created from a storyboard. The view controller's identifier is set. The search field's identifier is set as well. This view controller is a child view controller of another view controller.
I've read through the User Interface Preservation section of the "The Core App Design" document but it's unclear on how a view controller's views are saved/restored and how much of this is automatic versus manual.
Supporting OSX 10.12 and 10.11. Objective-C or Swift in any answers is fine.
To achieve aforementioned effect let's create NSSearchField and a custom class named RestoredWindow containing just one property:
import Cocoa
class RestoredWindow: NSWindow {
override class var restorableStateKeyPaths: [String] {
return ["self.contentViewController.searchField.stringValue"]
}
}
Assign this custom class to Window Controller in Identity Inspector.
Next, let's bind searchField.stringValue property to ViewController in Value section of Bindings Inspector.
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var searchField: NSSearchField!
override func viewDidLoad() {
super.viewDidLoad()
}
}
After this, make sure you haven't checked Close windows when quitting an app option in your System Preferences -> General tab.
Now, entered text in NSSearchField restored after quitting and launching the app again.
P.S.
I've tried the same way as you but failed too. This approach doesn't work for me:
override func encodeRestorableState(with coder: NSCoder) {
coder.encode(searchField.stringValue, forKey: "restore")
super.encodeRestorableState(with: coder)
}
override func restoreState(with coder: NSCoder) {
if let state = coder.decodeObject(forKey: "restore") as? NSSearchField {
searchField.stringValue = state
}
super.restoreState(with: coder)
}