How to Implement NSSlider Accessible for VoiceOver on MacOS? - swift

I have a slider:NSSlider and valueLabel:NSTextField, and I'm wondering what's the proper way to make it accessible for VoiceOver users.
First I connected a send action for slider to sliderChanged function to update valueLabel.
valueLabel.stringValue = String(slider.integerValue)
VoiceOver reads the label correctly, but it reads the slider in percentage. To fix this, I changed sliderChanged function to setAccessibilityValueDescription.
slider.setAccessibilityValueDescription(String(slider.integerValue))
Now VoiceOver correctly reads the value for the slider. However, it sees both valueLabel and slider, so it's redundant.
I tried valueLabel.setAccessibilityElement(false), but VoiceOver doesn't seem to ignore.
Could someone advise what would be the proper way to implement this? Thanks!

The best way to do this is to create a custom "ContainerView" class (which inherits from UIView) that contains the label and the slider, make the ContainerView an accessibilityElement, and set its accessibilityTraits to "adjustable." By creating a ContainerView that holds both the valueLabel and the slider, you remove the redundancy that is present in your current implementation, while not affecting the layout or usability of the slider/valueLabel for a non-VoiceOver user. This answer is based on this video, so if something is unclear or you want more in-depth info, please watch the video!
Setting a view's UIAccessibilityTraits to be "Adjustable" allows you to use its functions accessibilityIncrement and accessibilityDecrement, so that you can update whatever you need to (slider, textfield, etc). This trait allows any view to act like a typical adjustable (without having to add UIGestureRecognizers or additional VoiceOver announcements).
I posted my code below for convenience, but it is heavily based on the video that I linked to above. (I personally am an iOS developer, so my Swift code is iOS-based)
Note -- I had to override the "accessibilityValue" variable -- this was to make VoiceOver announce changes in the slider whenever the user swiped up or down.
My ContainerView class contains the following code:
class ContainerView: UIView {
static let LABEL_TAG = 1
static let SLIDER_TAG = 2
var valueLabel: UILabel {
return self.viewWithTag(ContainerView.LABEL_TAG) as! UILabel
}
var slider: UISlider {
return self.viewWithTag(ContainerView.SLIDER_TAG) as! UISlider
}
override var accessibilityValue: String? {
get { return valueLabel.text }
set {}
}
override var isAccessibilityElement: Bool {
get { return true }
set { }
}
override var accessibilityTraits: UIAccessibilityTraits {
get { return UIAccessibilityTraitAdjustable }
set { }
}
override init(frame: CGRect) {
super.init(frame: frame)
valueUpdated()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
valueUpdated()
}
func valueUpdated() {
valueLabel.text = String(slider.value)
slider.sendActions(for: .valueChanged)
}
override func accessibilityIncrement() {
super.accessibilityIncrement()
slider.setValue(slider.value + 1, animated: true)
valueUpdated()
}
override func accessibilityDecrement() {
super.accessibilityDecrement()
slider.setValue(slider.value - 1, animated: true)
valueUpdated()
}
}
Hope this helps!

Related

NSTouchBar not releasing last touched view

I am making an app which uses the NSTouchBar.
The touchbar is made by the NSWindowController.makeTouchBar()method.
In this touchbar I can place NSCustomTouchBarItems.
I have made two NSCustomTouchBarItems.
The first one sets a view to a default ui button, with this:
let item001 = NSCustomTouchBarItem(identifier: someIdentifier)
item001.view = NSButton(title: "myButton", target: nil, action: nil)
The second one sets a viewController, with this:
let item002 = NSCustomTouchBarItem(identifier: someIdentifier)
item002.viewController = TestViewController()
The TestViewController only loads a simple view inside its loadView()
method.
class TestViewController: NSViewController {
override func loadView() {
self.view = TestView001(frame: NSRect(x: 0, y: 0, width: 100, height: 30))
}
}
The TestView001 only creates a background color so you can see it.
TestView001 has the following code:
class TestView001: NSView {
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
print("TestView001.init()")
// Create a background color.
self.wantsLayer = true
self.layer?.backgroundColor = NSColor.green.cgColor
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
All of this works perfectly.
But when I have touched the second item inside the touchbar,
and then close my app's window.
The windowController and everything else is nicely released
from memory.
But I can still see that TestView001 is in memory and not being
released.
When using a standard ui button like in item001, then you don't
have this problem.
It looks like some NSTouch still has a reference to the view
if you look at this image:
However, I do not completely understand this image.
What is the best way of solving this.
Thanks in advance.

NSComboBox disappears immediately after click

I'm working on an application which behaves similarly to Spotlight. However, currently I'm having a trouble with NSComboBox.
To show the application after hitting a hotkey I use activate(ignoringOtherApps: true) and NSWindowController(window: window).showWindow(self). Then when user hits Escape I do window.close() and hide(self).
Everything works great, previous application gets focus. However, when I open again the window, first click on NSComboBox causes a very strange behavior (like on the movie below). First time it instantly disappears.
I found out that it happens because of NSApp.hide. When I don't call it, everything works well. However, I need to call it, because I want the previous app to get the focus.
To workaround this issue I can replace NSWindow with nonactivating NSPanel. It resolves the problem. However, it's not possible in my case because I need to use it also with presentAsModalWindow and presentAsSheet where I can't control whether it's a window or panel.
I also discovered that a single click on window's background before clicking on ComboBox helps. So it seems like this window doesn't have focus, but looks like focused. I also tried all methods like makeKeyAndOrderFront, becomeFirstResponder, NSApp.unhide etc. etc. Nothing helps.
Under the hood NSComboBox has its own window NSComboBoxWindow, so my guess is that when I click it opens its window and then it receives information that the parent window took focus and dismisses itself for some reason.
I'm not sure if this is Cocoa bug or what. Is there any way to fix it?
Minimum Reproducible Example
Create new macOS project with NSComboBox and NSButton. Connect button to IBAction.
import Cocoa
class ViewController: NSViewController {
#IBAction func close(_ sender: Any) {
view.window?.close()
NSApp.hide(self)
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) {
self.view.window?.makeKeyAndOrderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
}
}
Workaround
Finally, I managed to create a workaround. It's ugly but it works. I cover arrow part with a transparent view, intercept click and invoke two times expand via accessibility...
import Cocoa
final class ClickView: NSView {
var onMouseDown: () -> (Bool) = { return false }
override func mouseDown(with event: NSEvent) {
if !onMouseDown() {
super.mouseDown(with: event)
}
}
}
final class FixedComboBox: NSComboBox {
private let clickView = ClickView()
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
fix()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func awakeFromNib() {
super.awakeFromNib()
fix()
}
private func fix() {
clickView.onMouseDown = { [weak self] in
guard let cell = self?.cell else { return false }
// first expand will be immediately closed because of Cocoa bug
cell.setAccessibilityExpanded(true)
// we need to schedule another one with a small delay to let it close the first call
// this one almost immediately to avoid blinking
DispatchQueue.main.async { cell.setAccessibilityExpanded(true) }
// in case the first one didn't "catch" the right moment (sometimes it happens)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { cell.setAccessibilityExpanded(true) }
return true
}
addSubview(clickView)
clickView.snp.makeConstraints { make in
make.width.equalTo(20)
make.trailing.top.bottom.equalToSuperview()
}
}
}

Where to ctrl-drag IBOutlets, view class or ViewController?

I'm pretty new to coding. Im not sure if an IBOutlet (button, text field, etc) ctrl-dragged from a xib should go in the xib's NSView class or in the view controller which has the NSView added as a subview.
I've been playing around with this for a while, learning as I go. I'm stuck on wondering if I have the code structured correctly. This is for MacOS so resources are limited and often dated. I'd assume that an outlet added for a button, for example, would go in the controller as views should be "dumb". If I try that the actions always have "action" set automatically and type as Any as a default - not what I'm used to seeing. I suspect this may have something to do with the class set for the file's owner and the class set for the view in IB. If anyone can outline the best way to handle this that would be fantastic, thank you!
The view that loads the xib:
class View4: NSView {
#IBOutlet weak var view: View4!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
Bundle.main.loadNibNamed("View4", owner: self, topLevelObjects: nil)
self.frame = self.bounds
self.wantsLayer = true
self.translatesAutoresizingMaskIntoConstraints = false
self.layer?.backgroundColor = NSColor.purple.cgColor
self.roundedCorners(on: self)
// add xib to custom NSView subclass
self.addSubview(self.view)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
}
The corresponding ViewController:
class View4Controller: NSViewController {
override func loadView() {
print("View4Controller.loadView")
self.view = NSView()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
print("View4Controller.viewDidLoad")
self.view = View4()
}
}
The idea of an outlet is to have a reference to an object that is outside of your code created. The concept is great for prototyping, but tends to become hard to manage as a project grow.
If you class is the class, then it can refer to itself. („self“ in swift or „this“ in c++) You don't need an outlet in this case.
The outlet is normally used by controller that need to maintain the view. The concept is a alternative to creating and configuring the view manually.

Any way to opt out of autoresizing permanently?

I'm writing nib-less views in which I use autolayout for all my layout logic. I find myself having to turn off autoresizing with every view I instantiate. My code is littered with a lot of these:
view.translatesAutoresizingMaskIntoConstraints
Ideally I'd like to just
extension UIView/NSView {
override var translatesAutoresizingMaskIntoConstraints: Bool = false
}
and get it over with once and for all, but extensions can't override stored properties.
Is there some other simple way to switch off autoresizing for good?
Well just a suggestion since its annoying to always set that to false, just setup a function with all the shared setups for the UIView and call it every time,
its saves time and its kinda less annoying than trying and setting the values each time,
extension UIView {
func notTranslated() {
self.translatesAutoresizingMaskIntoConstraints = false
//Add any additional code.
}
}
//Usage
let view = UIView()
view.notTranslated()
You can't override this constraints properties because the UIView maybe declared in the IB
translatesAutoresizingMaskIntoConstraints according to apple.
By default, the property is set to true for any view you programmatically create. If you add views in Interface Builder, the system automatically sets this property to false.
imagine if you could override that from an extension that would lead to some conflicts if there was other UIView's that's have the opposite value True || false, so in my opinion:
Apple did this to prevent any conflicts with the views constrains, therefore if you don't like to write it every time just wrap it up in a function.
Please if anyone have additional information, don't hesitate to contribute.
UPDATE: I found this cool answer that could also work, check out the code below.
class MyNibless: UIView {
//-----------------------------------------------------------------------------------------------------
//Constructors, Initializers, and UIView lifecycle
//-----------------------------------------------------------------------------------------------------
override init(frame: CGRect) {
super.init(frame: frame)
didLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
didLoad()
}
convenience init() {
self.init(frame: CGRect.zero)
}
func didLoad() {
//Place your initialization code here
//I actually create & place constraints in here, instead of in
//updateConstraints
}
override func layoutSubviews() {
super.layoutSubviews()
//Custom manually positioning layout goes here (auto-layout pass has already run first pass)
}
override func updateConstraints() {
super.updateConstraints()
//Disable this if you are adding constraints manually
//or you're going to have a 'bad time'
//self.translatesAutoresizingMaskIntoConstraints = false
translatesAutoresizingMaskIntoConstraints = false
//Add custom constraint code here
}
}
var nibless: UIView = MyNibless()
//Usage
nibless.updateConstraints()
print(nibless.translatesAutoresizingMaskIntoConstraints) //false
So simply just create MyNibless instance as UIView and this also open big door to customizations too

Avoid boilerplate code to setup UI in viewDidLoad

i am just wondering how did you properly setup your UI in your IOS developments with Swift. Generally, I feel like I need to put a lot of statements in viewDidLoad lifecycle method of a view controller to customize UI elements. I know that I can use storyboard to help to setup those UI elements but sometimes we need to make some adjustments programmatically. Those adjustments resulting in a huge and boilerplate code in viewDidLoad. So, how do you handle this ? Did your use extensions only for the UI part ? Specific classes ? How you can clearly separate UI from logic ?
Make a custom view for it!
If you find yourself writing a lot of this kind of code:
myView.someProperty1 = someValue1
myView.someProperty2 = someValue2
myView.someProperty3 = someValue3
myView.someProperty4 = someValue4
myView.someProperty5 = someValue5
myView.addSubView(subView1)
myView.addSubView(subView2)
myView.addSubView(subView3)
...
and the values that you give the properties are all independent of the view controller, it might be time to create a custom view.
Here is an example:
Create an xib file for your view, and name it the same name as your custom view. You will be adding the subviews of your custom view and all the constraints you need here.
And then you can do something like this:
#INDesignable // add this if you want to see your view drawn on the storyboard!
class MyCustomView: UIView {
#IBOutlet var subView1: UIImageView!
#IBOutlet var subView2: UITextField!
#IBOutlet var subView3: UIImageView!
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
let view = viewFromNibForClass()
view.frame = bounds
view.autoresizingMask = [
UIViewAutoresizing.flexibleWidth,
UIViewAutoresizing.flexibleHeight
]
addSubview(view)
// set up your view here...
// set all the properties and stuff
}
private func viewFromNibForClass() -> UIView {
let bundle = Bundle(for: MyCustomView.self)
let nib = UINib(nibName: String(describing: type(of: self)), bundle: bundle)
let view = nib.instantiate(withOwner: self, options: nil).first as! UIView
return view
}
}