NSButton turns gray when pushed - swift

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

Related

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

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")
}
}

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
}

RealityKit Custom ARAnchor not syncing across devices

I'm using Apple's custom ARAnchor in a config.isCollaborationEnabled = true environment.
When I call the following on DeviceA:
let boardAnchor = BoardAnchor(transform: last.worldTransform, size: CGSize(width: 10, height: 11))
arView.session.add(anchor: boardAnchor)
I can see the delegate func session(_ session: ARSession, didAdd anchors: [ARAnchor]) get called with the BoardAnchor on DeviceA.
However, DeviceB does not receive such a delegate call.
If however I add a non-subclassed ARAnchor on DeviceA, I can see the delegate called on DeviceB.
let namedAnchor = ARAnchor(name: "test", transform: last.worldTransform)
arView.session.add(anchor: namedAnchor)
So I'm really confused as to why the subclass doesn't work...any ideas?
class BoardAnchor: ARAnchor {
let size: CGSize
init(transform: float4x4, size: CGSize) {
self.size = size
super.init(name: "Board", transform: transform)
}
override class var supportsSecureCoding: Bool {
return true
}
required init?(coder aDecoder: NSCoder) {
self.size = aDecoder.decodeCGSize(forKey: "size")
super.init(coder: aDecoder)
}
// this is guaranteed to be called with something of the same class
required init(anchor: ARAnchor) {
let other = anchor as! BoardAnchor
self.size = other.size
super.init(anchor: other)
}
override func encode(with aCoder: NSCoder) {
super.encode(with: aCoder)
aCoder.encode(size, forKey: "size")
}
}
Delegate
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
DLog("didAdd anchor: \(anchor)")
if anchor.name == "test" {
// Non-sublcass ARAnchor ok
}
if let board = anchor as? BoardAnchor {
// Never called
}
}
}
I believe this line from the Building Collaborative AR Experiences session (WWDC 2019) might explain the issue you're facing;
Last, only the user created ARAnchors are shared. That excludes all
the subclass ARAnchors, including ARImageAnchor, ARPlaneAnchor, and
ARObjectAnchor. That also excludes the user subclass ARAnchor which
were used to attach user data within Map Save and Load.
The session seems to go on to indicate that in lieu of using a sublcassed ARAnchor, you can define your own Entity component and conform your Entity to that protocol, hereby allowing you to negate having to use a subclassed ARAnchor and allowing the component, which would synchronize across the session, to perform a similar task. However, if you are not using RealityKit, and are, instead, using SceneKit or SpriteKit, you will likely need to determine a different methodology, such as avoiding subclassing ARAnchor and move your logic in BoardAnchor somewhere else.
I think that other than simply noting the config.isCollaborationEnabled = true, you need to manually handle the data sent out and the connectivity and other peers.
In addition, there is a different delegate method to get the collaborated anchors, if I remember correctly.
Please note the following:
https://developer.apple.com/documentation/arkit/creating_a_collaborative_session
There is a sample project there that will probably answer most of your questions..

Why do these NSView swift subclesses allow initialisation to run twice

In the Swift subclasses below, when ChildView(dummy: NSObject()) is called, two different designated initializers for ParentView will be invoked, and the myLayer ivar of ParentView will be initialized twice.
Not that this will not happen in a Playground - that will correctly report that error: MyPlayground.playground:5:31: error: must call a designated initializer of the superclass 'NSView'
However, in an Xcode project (e.g. a Command Line Tool), this will compile and run fine.
I understand a lot of what's going on, and how to prevent this problem. But I'd love to understand why the compiler is not erroring, or at least warning me about this code (and preventing the double initialisation)
This code compiles cleanly under Swift 4.2, and in the debugger, I can see exactly what's happening:
When ChildView(dummy:) is called, it invokes ParentView(dummy:)
That's a designated initialiser, do the ParentView ivars are also initialised (myLayer)
Then ParentView(dummy:) calls super.init(), which is NSView.init()
The implementation of NSView.init() calls initWithFrame, so ChildView.init(frame:) gets invoked, and invokes ParentView.init(frame:). That's also a designated initialiser, so
myLayer gets initialised a second time.
To demonstrate this, given the classes below, invoke
let _ = ChildView(dummy: NSObject)
>>>> creating layer will get printed twice.
The way to avoid this would be to have ParentView(dummy:) call super.init(frame:), and then the problem goes away.
My question is really, how could this ever happen in the first place?
import Cocoa
class ParentView: NSView {
private var myLayer: CALayer = {
print(">>>> creating layer")
return CALayer()
}()
override init(frame frameRect: NSRect) { super.init(frame: frameRect) }
required init?(coder decoder: NSCoder) { super.init(coder: decoder) }
init(dummy: AnyObject?) { self.init() }
}
class ChildView: ParentView {
override init(frame frameRect: NSRect) { super.init(frame: frameRect) }
required init?(coder decoder: NSCoder) { super.init(coder: decoder) }
override init(dummy: AnyObject?) { super.init(dummy: dummy) }
}
This should generate a compile time error, as it does in a Playground.
I can't seem to construct a similar non-NSView class hierarchy that gives the same behaviour. No matter how I tweak the hierarchy, constructors, etc., Swift always gives compiler errors when I try to this with new objects (even when I mix Objective-C and Swift, to be analagous to NSView.
I can't find any public declaration for NSView.init() either, although I can see it executing in the debugger.

Object is nil although it was set in init

I have an object called navigator, which I set within init. I break on it to make sure it is set. However when an IBAction func, linkButtonClicked, get's called and try's to use navigator I get a nil exception. Why?
class HomeCollectionViewCell: UICollectionViewCell {
let appDelegate:AppDelegate!
let navigator: Navigator!
#IBOutlet weak var linkButton: UIButton!
var destinationView:String?
var parentViewController:UIViewController?
#IBAction func linkButtonClicked(_ sender: Any) {
do {
try self.navigator.navigate(to: self.destinationView!, from: parentViewController!)
} catch {
}
}
required init?(coder aDecoder: NSCoder) {
self.appDelegate = UIApplication.shared.delegate as! AppDelegate
self.navigator = self.appDelegate.navigator
super.init(coder: aDecoder)
}
override func prepareForReuse() {
super.prepareForReuse()
// do resetting here if needed, like empty out data
linkButton.setTitle(nil, for: .normal)
}
}
The init?(coder: NSCoder) initializer gets used when you are retrieving the object from some kind of encoded store such as Core Data. This initializer is required by the NSCoding protocol and is used only for deserializing the object. Therefore, it does not get called at object creation. It only gets called if you serialize the object using NSCoding and later deserialize it.
The function you want to override in order to ensure some value will be set in your view is not its init (and if you really want to use its init, the method to overload is init(frame:)). Instead, you should set any variables you want to be available in the viewDidLoad method of the view controller.