AccessibilityUIExamples/Switches/TwoPositionSwitchView.swift
/* |
See LICENSE folder for this sample’s licensing information. |
Abstract: |
An example demonstrating making an accessible, custom two-position switch. |
*/ |
import Cocoa |
/* |
IMPORTANT: This is not a template for developing a custom switch. |
This sample is intended to demonstrate how to add accessibility to |
existing custom controls that are not implemented using the preferred methods. |
For information on how to create custom controls please visit http://developer.apple.com |
*/ |
class TwoPositionSwitchCell: NSSliderCell { |
enum TrackStates: Int { |
case knobClickedState = 0 |
case trackClickedState |
case knobMovedState |
case knobNoState |
} |
var trackingState = 0 |
// MARK: - Drawing |
override func knobRect(flipped: Bool) -> NSRect { |
let value = doubleValue |
let percent: Double = (maxValue <= minValue) ? 0.0 : (value - minValue) / (maxValue - minValue) |
let imageName = "SwitchHandle" |
let knobImage = NSImage(named: NSImage.Name(rawValue: imageName))! |
var knobRect = NSRect(x: 0, y: 0, width: knobImage.size.width, height: knobImage.size.height) |
let offset = floor(CGFloat(trackRect.width - knobRect.width) * CGFloat(percent)) |
knobRect.origin = NSPoint(x: trackRect.origin.x + offset, y: 2.0) |
return knobRect.integral |
} |
override func drawKnob(_ knobRect: NSRect) { |
let isClickedOrMoved = |
trackingState == TrackStates.knobClickedState.rawValue || |
trackingState == TrackStates.knobMovedState.rawValue |
let knobImage = NSImage(named: NSImage.Name(rawValue: isClickedOrMoved ? "SwitchHandleDown" : "SwitchHandle"))! |
knobImage.draw(at: knobRect.origin, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: (isEnabled ? 1.0 : 0.5)) |
} |
override func drawBar(inside rect: NSRect, flipped: Bool) { |
// avoid drawing the track |
} |
override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) { |
guard let controlView = controlView as? NSControl else { return } |
// Draw the switch background. |
var imageName = "SwitchWell" |
let wellImage = NSImage(named: NSImage.Name(rawValue: imageName))! |
var trackPoint = cellFrame.origin |
trackPoint.y += 1.0 |
if controlView.isEnabled { |
wellImage.draw(at: trackPoint, from: NSRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0) |
} else { |
wellImage.draw(at: trackPoint, from: NSRect.zero, operation: NSCompositingOperation.plusLighter, fraction: 1.0) |
wellImage.draw(at: trackPoint, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 0.3) |
} |
// Draw the switch overlay. |
imageName = "SwitchOverlayMask" |
let maskImage = NSImage(named: NSImage.Name(rawValue: imageName))! |
trackPoint.y -= 1.0 |
let focused = showsFirstResponder && focusRingType != NSFocusRingType.none |
if focused { |
maskImage.draw(at: trackPoint, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0) |
} |
super.drawInterior(withFrame: cellFrame, in:controlView) |
if !focused { |
maskImage.draw(at: trackPoint, from: NSRect.zero, operation: NSCompositingOperation.sourceOver, fraction: 1.0) |
} |
} |
// MARK: - Tracking |
override func startTracking(at startPoint: NSPoint, in controlView: NSView) -> Bool { |
guard let ourControl = controlView as? TwoPositionSwitchView else { return false } |
guard !ourControl.isAnimating else { return false } |
// Don't track if mouseDown is not on the knob. |
let knobRect = self.knobRect(flipped: controlView.isFlipped) |
if knobRect.contains(startPoint) { |
trackingState = TrackStates.knobClickedState.rawValue |
return super.startTracking(at: startPoint, in: controlView) |
} |
trackingState = TrackStates.trackClickedState.rawValue |
return true |
} |
override func continueTracking(last lastPoint: NSPoint, current currentPoint: NSPoint, in controlView: NSView) -> Bool { |
if (trackingState == TrackStates.knobClickedState.rawValue) && |
!(lastPoint == currentPoint) && |
!(lastPoint == NSPoint()) { |
trackingState = TrackStates.knobMovedState.rawValue |
} |
return super.continueTracking(last: lastPoint, current: currentPoint, in: controlView) |
} |
override func stopTracking(last lastPoint: NSPoint, current stopPoint: NSPoint, in controlView: NSView, mouseIsUp flag: Bool) { |
super.stopTracking(last: lastPoint, current: stopPoint, in: controlView, mouseIsUp: flag) |
guard let ourControl = controlView as? TwoPositionSwitchView else { return } |
let startValue = ourControl.targetValue |
let value = doubleValue |
switch trackingState { |
case TrackStates.knobClickedState.rawValue, TrackStates.trackClickedState.rawValue: |
ourControl.setDoubleValue(value: (startValue == 0.0) ? 1.0 : 0.0, animate:true) |
case TrackStates.knobMovedState.rawValue: |
if abs(startValue - value) < 0.2 { |
ourControl.setDoubleValue(value: startValue, animate: true) |
} else { |
ourControl.setDoubleValue(value: (startValue == 0.0) ? 1.0 : 0.0, animate: true) |
} |
default: break |
} |
trackingState = TrackStates.knobNoState.rawValue |
} |
} |
// MARK: - |
class TwoPositionSwitchView: NSSlider { |
// MARK: - Internals |
fileprivate var isAnimating: Bool { |
return targetValue != doubleValue |
} |
var targetValue: Double = 0 |
// MARK: - View Lifecycle |
required override init(frame frameRect: NSRect) { |
let name = "SwitchOverlayMask" |
let maskImage = NSImage(named: NSImage.Name(rawValue: name))! |
let rect = NSRect(x: 0, y: 0, width: maskImage.size.width, height: maskImage.size.height) |
super.init(frame: rect) |
commonInit() |
} |
required init?(coder aDecoder: NSCoder) { |
super.init(coder: aDecoder) |
commonInit() |
} |
fileprivate func commonInit() { |
doubleValue = 0.0 |
minValue = 0.0 |
maxValue = 1.0 |
isContinuous = false |
} |
override class var cellClass: AnyClass? { |
// We want our cell to be custom. |
get { |
return TwoPositionSwitchCell.self |
} |
set { |
fatalError("Setter should not be called.") |
} |
} |
// Used to customize the animation duration of the knob across the switch track. |
override static func defaultAnimation(forKey key: NSAnimatablePropertyKey) -> Any? { |
if key.rawValue == "doubleValue" { |
return defaultAnimation(forKey: NSAnimatablePropertyKey(rawValue: "frameOrigin")) |
} else { |
return super.defaultAnimation(forKey: key) |
} |
} |
override var isFlipped: Bool { |
return false |
} |
fileprivate func setState(value: Int) { |
setState(value: value, animate: true) |
} |
fileprivate func setState(value: Int, animate: Bool) { |
let value = (value == NSControl.StateValue.on.rawValue) ? 1.0 : 0.0 |
if value != targetValue { |
setDoubleValue(value: value, animate: animate) |
} |
} |
fileprivate var state: NSInteger { |
let targetValue = self.targetValue |
if targetValue == 0.0 { |
return NSControl.StateValue.off.rawValue |
} else if targetValue == 1.0 { |
return NSControl.StateValue.on.rawValue |
} else { |
return NSControl.StateValue.mixed.rawValue |
} |
} |
// MARK: - Keyboard Events |
// Set to allow keyDown, moveLeft, moveRight, etc. to be called. |
override var acceptsFirstResponder: Bool { return true } |
override func moveRight(_ sender: Any?) { |
if isEnabled { |
setDoubleValue(value: 1.0, animate:true) |
sendAction(action, to:target) |
} |
} |
override func moveLeft(_ sender: Any?) { |
if isEnabled { |
setDoubleValue(value: 0.0, animate:true) |
sendAction(action, to:target) |
} |
} |
override func moveUp(_ sender: Any?) { |
moveRight(sender) |
} |
override func moveDown(_ sender: Any?) { |
moveLeft(sender) |
} |
override func pageUp(_ sender: Any?) { |
moveRight(sender) |
} |
override func pageDown(_ sender: Any?) { |
moveLeft(sender) |
} |
fileprivate func setDoubleValue(value: Double, animate: Bool) { |
targetValue = value |
if doubleValue != value { |
if animate { |
NSAnimationContext.current.duration = 0.15 * abs(value - doubleValue) |
animator().doubleValue = value |
} else { |
doubleValue = value |
} |
} |
} |
} |
// MARK: - |
extension TwoPositionSwitchView { |
// MARK: NSAccessibilitySwitch |
override func accessibilityValue() -> Any? { |
return integerValue == 0 ? |
NSLocalizedString("off", comment: "accessibility value for the state of OFF for the switch") : |
NSLocalizedString("on", comment: "accessibility value for the state of ON for the switch") |
} |
override func accessibilityLabel() -> String? { |
return NSLocalizedString("Switch", comment: "accessibility label of the two position switch") |
} |
override func accessibilityPerformPress() -> Bool { |
// User did control-option-space keyboard shortcut. |
let isStateOff = state == NSControl.StateValue.off.rawValue |
setState(value: isStateOff ? NSControl.StateValue.on.rawValue : NSControl.StateValue.off.rawValue) |
return true |
} |
} |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-09-12