swiftmacosbecomefirstresponder

How to set NSButton.isEnabled from subclassed NSTextField


I'm very new to Swift MacOS programming and have been learning by writing small test applications.

The aim of this application is to enable the pushbutton when the 2nd textfield has the focus, and disable it when it is not focused.

I have found that by subclassing the NSTextField I can override becomeFirstResponder() however don't know how to set the button to be disabled from the subclass.

ViewController:

class ViewController: NSViewController {

    @IBOutlet public weak var pushButton: NSButton!

    @IBOutlet weak var textField3: NSTextField!
    @IBOutlet weak var textField2: GSTextField!
    @IBOutlet weak var textField1: NSTextField!


    override func viewDidLoad() {
        super.viewDidLoad()

        textField2.delegate = self

    // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
        // Update the view, if already loaded.
        }
    }

    func chgButton(onoff: Bool){
        pushButton.isEnabled = onoff
    }

}

// When the field completes editing make the pushbutton disabled.
extension ViewController: NSTextFieldDelegate {
    override func controlTextDidEndEditing(_ obj: Notification) {
        print("did end")
        chgButton(onoff: false)

    }
}

GSTextField.Swift

class GSTextField: NSTextField {

    override func becomeFirstResponder() -> Bool {
        print("GSTextField Firstresponder")
 ////*** I need to set the button to be enabled here
        return super.becomeFirstResponder()
    }
 }   

Solution

  • Your NSTextField subclass needs to be able to communicate with the pushButton. The easiest way to do this is to pass a reference to the pushButton to your text field and then update the push button from there.

    Update your ViewController like this:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        textField2.delegate = self
        textField2.pushButton = pushButton
    
        // Do any additional setup after loading the view.
    }
    

    And your GSTextField like this:

    class GSTextField: NSTextField {
    
        weak var pushButton: NSButton?
    
        override func becomeFirstResponder() -> Bool {
            print("GSTextField Firstresponder")
            pushButton?.isEnabled = true
            return super.becomeFirstResponder()
        }
    
        override func resignFirstResponder() -> Bool {
            pushButton?.isEnabled = false
            return super.resignFirstResponder()
        }
    }
    

    It should be noted that while this works fine in this toy example, this is a sub-optimal solution to this problem because it tightly couples the pushButton and the GSTextField. A better solution would be to use delegation to communicate the focus changes to the ViewController, and let the ViewController handle the updates.

    Your GSTextField would look like this:

    protocol FocusObservable: class {
        func didGainFocus(sender: Any)
        func didLoseFocus(sender: Any)
    }
    
    class GSTextField: NSTextField {
    
        weak var focusDelegate: FocusObservable?
    
        override func becomeFirstResponder() -> Bool {
            print("GSTextField Firstresponder")
            focusDelegate?.didGainFocus(sender: self)
            return super.becomeFirstResponder()
        }
    
        override func resignFirstResponder() -> Bool {
            focusDelegate?.didLoseFocus(sender: self)
            return super.resignFirstResponder()
        }
    }
    

    And then you would add protocol conformance to the ViewController:

    extension ViewController: FocusObservable {
        func didGainFocus(sender: Any) {
            pushButton.isEnabled = true
        }
    
        func didLoseFocus(sender: Any) {
            pushButton.isEnabled = false
        }
    }
    

    and set the focusDelegate of the text field:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        textField2.delegate = self
        textField2.focusDelegate = self
        // Do any additional setup after loading the view.
    }