Capable
Keep track of accessibility settings, leverage high contrast colors, and use scalable fonts to enable users with disabilities to use your app.
Accessibility for iOS, macOS, tvOS, and watchOS.
Installation
There are currently four different ways to integrate Capable into your apps.
CocoaPods
use_frameworks!
target 'MyApp' do
# all features + color and font extensions
pod 'Capable'
# all features, but exclude color and font extensions
pod 'Capable/Features'
# color extensions only
pod 'Capable/Colors'
# font extensions only
pod 'Capable/Fonts'
end
Carthage
github "chrs1885/Capable"
Swift Package Manager
dependencies: [
.package(url: "https://github.com/chrs1885/Capable.git", from: "1.0.1")
]
Manually
Simply drop Capable.xcodeproj
into your project. Also make sure to add
Capable.framework
to your app’s embedded frameworks found in the General tab of your main project.
Usage
Register for (specific) accessibility settings
Firstly, you need to import the Capable framework in your class by adding the following import statement:
import Capable
There are two different ways to initialize the framework instance. You can either set it up to consider all accessibility features
let capable = Capable()
or by passing in only specific feature names
let capable = Capable(withFeatures: [.largerText, .boldText, .shakeToUndo])
You can find a list of all accessibility features available on each platform in the accessibility feature overview section.
Get accessibility status
If you are interested in a specific accessibility feature, you can retrieve its current status as follows:
let capable = Capable()
let isVoiceOverEnabled: Bool = capable.isFeatureEnable(feature: .voiceOver)
To get a dictionary of all features, that the Capable
instance has been initialized with you can use:
let capable = Capable()
let statusMap = capable.statusMap
This will return each feature name (key) along with its current value as described in the accessibility feature overview section.
Handicaps - grouped accessibility features
You can also group accessibility features to represent a specific handicap:
// Define a set of features that represent a handicap
let features: [CapableFeature] = [.voiceOver, .speakScreen, .speakSelection]
// Use the Handicap object to group them
let blindness = Handicap(features: features, name: "Blindness", enabledIf: .allFeaturesEnabled)
// Initialize the framework instance by providing the Handicap
let capable = Capable(withHandicaps: [blindness])
The value of the name
parameter will be used inside the statusMap
provided by the Capable framework instance. Based on the value of enabledIf
, you can specify if all features need to be set to enabled in order to set the Handicap to enabled as well.
Just like accessibility features, the Handicap
type works great with notifications.
Send accessibility status
The statusMap
object is compatible with most analytic SDK APIs. Here's a quick example of how to send your data along with user properties or custom events.
func sendMetrics() {
let statusMap = self.capable.statusMap
let eventName = "Capable features received"
// App Center
MSAnalytics.trackEvent(eventName, withProperties: statusMap)
// Firebase
Analytics.logEvent(eventName, parameters: statusMap)
// Fabric
Answers.logCustomEvent(withName: eventName, customAttributes: statusMap)
}
Listen for settings changes
After initialization, notifications for all features that have been registered can be retrieved. To react to changes, you need to add your class as an observer as follows:
NotificationCenter.default.addObserver(
self,
selector: #selector(self.featureStatusChanged),
name: .CapableFeatureStatusDidChange,
object: nil)
Inside your featureStatusChanged
you can parse the specific feature and value:
@objc private func featureStatusChanged(notification: NSNotification) {
if let featureStatus = notification.object as? FeatureStatus {
let feature = featureStatus.feature
let currentValue = featureStatus.statusString
}
}
If your framework instance has been set up with Handicap
s instead, you can use the CapableHandicapStatusDidChange
notification:
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handicapStatusChanged),
name: .CapableHandicapStatusDidChange,
object: nil)
Once the notification has been sent, you can parse the Handicap
and its current status as follows:
@objc private func handicapStatusChanged(notification: NSNotification) {
if let handicapStatus = notification.object as? HandicapStatus {
let handicap = handicapStatus.handicap
let currentValue = handicapStatus.statusString
}
}
Please note that when using notifications with Handicap
s on macOS or watchOS, you might not get notified about all changes since not all accessibility features do support notifications, yet.
High contrast colors (Capable UIColor/NSColor extension)
The Web Content Accessibility Guidelines (WCAG) define minimum contrast ratios for a text and its background. The Capable framework extends UIColor
and NSColor
with functionality to use WCAG conformant colors within your apps to help people with visual disabilities to perceive content.
Internally, the provided colors will be mapped to an equivalent of the sRGB color space. All functions will return nil
and log warnings with further info in case any input color couldn't be converted. Also note that semi-transparent text colors will be blended with its background color. However, the alpha value of semi-transparent background colors will be ignored since the underlying color can't be determined.
Text colors
Get a high contrast text color for a given background color as follows:
let textColor = UIColor.getTextColor(onBackgroundColor: UIColor.red)!
This will return the text color with the highest possible contrast (black/white). Alternatively, you can define a list of possible text colors as well as a required conformance level. Since the WCAG requirements for contrast differ in text size and weight, you also need to provide the font used for the text. The following will return the first text color that satisfies the required conformance level (AA by default).
let textColor = UIColor.getTextColor(
fromColors: [UIColor.red, UIColor.yellow],
withFont: myLabel.font,
onBackgroundColor: view.backgroundColor,
conformanceLevel: .AA
)!
Background colors
This will also work the other way round. If you are looking for a high contrast background color:
let backgroundColor = UIColor.getBackgroundColor(forTextColor: UIColor.red)!
// or
let backgroundColor = UIColor.getBackgroundColor(
fromColors: [UIColor.red, UIColor.yellow],
forTextColor: myLabel.textColor,
withFont: myLabel.font,
conformanceLevel: .AA
)!
Image captions (iOS/tvOS/macOS)
Get a high contrast text color for any given background image as follows:
let textColor = UIImage.getTextColor(onBackgroundImage: myImage imageArea: .bottomLeft)!
This will return the text color with the highest possible contrast (black/white) for a specific image Area.
Alternatively, you can define a list of possible text colors as well as a required conformance level. Since the WCAG requirements for contrast differ in text size and weight, you also need to provide the font used for the text. The following will return the first text color that satisfies the required conformance level (AA by default).
let textColor = UIImage.getTextColor(
fromColors: [UIColor.red, UIColor.yellow],
withFont: myLabel.font,
onBackgroundImage: view.backgroundColor,
imageArea: topLeft,
conformanceLevel: .AA
)!
You can find an overview of all image areas available in the documentation.
Calculating contrast ratios & WCAG conformance levels
The contrast ratio of two opaque colors can be calculated as well:
let contrastRatio: CGFloat = UIColor.getContrastRatio(forTextColor: UIColor.red, onBackgroundColor: UIColor.yellow)!
Once the contrast ratio has been determined, you can check the resulting conformance level specified by WCAG as follows:
let passedConformanceLevel = ConformanceLevel(contrastRatio: contrastRatio, fontSize: myLabel.font.pointSize, isBoldFont: true)
Here's an overview of available conformance levels:
Level | Contrast ratio | Font size |
---|---|---|
.A | Not specified for text color | - |
.AA | 3.0 | 18.0 (or 14.0 and bold) |
4.5 | 14.0 | |
.AAA | 4.5 | 18.0 (or 14.0 and bold) |
.AAA | 7.0 | 14.0 |
.failed | .AA/.AAA not satisfied | - |
Dynamic Type with custom fonts (Capable UIFont extension)
Supporting Dynamic Type along with different OS versions such as iOS 10 and iOS 11 (watchOS 3 and watchOS 4) can be a huge pain, since both versions provide different APIs.
Capable easily auto scales system fonts as well as your custom fonts by providing one line of code:
let myLabel = UILabel(frame: frame)
// Scalable custom font
let myCustomFont = UIFont(name: "Custom Font Name", size: defaultFontSize)!
myLabel.font = UIFont.scaledFont(for: myCustomFont)
// or
myLabel.font = UIFont.scaledFont(withName: "Custom Font Name", ofSize: defaultFontSize)
// Scalable system font
myLabel.font = UIFont.scaledSystemFont(ofSize: defaultFontSize)
// Scalable italic system font
myLabel.font = UIFont.scaledItalicSystemFont(ofSize: defaultFontSize)
// Scalable bold system font
myLabel.font = UIFont.scaledBoldSystemFont(ofSize: defaultFontSize)
While these extension APIs are available on tvOS as well, setting the font size in the system settings is not supported on this platforms.
Logging with OSLog
The Capable framework provides a logging mechanism that lets you keep track of what's going on under the hood. You'll get information regarding your current setup, warnings about anything that might cause issues further on, and errors that will lead to misbehavior.
By default, all messages will be logged automatically by using Apple's Unified Logging System. However, it also integrates with your specific logging environment by providing a custom closure that will be called instead. For example, you may want to send all errors coming from the Capable framework to your analytics service:
// Send error messages to your data backend
Capable.onLog = { message, logType in
if logType == OSLogType.error {
sendLog("Capable Framework: \(message)")
}
}
Furthermore, you can specify the minimum log level that should be considered when logging messages:
// Configure logger to only log warnings and errors (.default, .error, and .fault)
Capable.minLogType = OSLogType.default
Here's a list of the supported log types, their order, and what kind of messages they are used for:
OSLogType | Usage |
---|---|
.debug | Verbose logging * |
.info | Information regarding the framework setup and status changes |
.default | Warnings that may lead to unwanted behavior |
.error | Errors caused by the framework |
.fault | Errors caused by the framework due to system issues * |
* Currently not being used by the framework when logging messages.
Accessibility feature overview
The following table contains all features that are available AND settable on each platform.
iOS | macOS | tvOS | watchOS | |
---|---|---|---|---|
.assistiveTouch | :white_check_mark: | |||
.boldText | :white_check_mark: | :white_check_mark: | :white_check_mark:* | |
.closedCaptioning | :white_check_mark: | :white_check_mark: | ||
.darkerSystemColors | :white_check_mark: | |||
.differentiateWithoutColor | :white_check_mark: | |||
.fullKeyboardAccess | :white_check_mark:* | |||
.grayscale | :white_check_mark: | :white_check_mark: | ||
.guidedAccess | :white_check_mark: | |||
.hearingDevice | :white_check_mark: | |||
.increaseContrast | :white_check_mark: | |||
.invertColors | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
.largerText | :white_check_mark: | :white_check_mark:* | ||
.monoAudio | :white_check_mark: | :white_check_mark: | ||
.reduceMotion | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
.reduceTransparency | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
.shakeToUndo | :white_check_mark: | |||
.speakScreen | :white_check_mark: | |||
.speakSelection | :white_check_mark: | |||
.switchControl | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
.voiceOver | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
* Feature status can be read but notifications are not available.
While most features can only have a statusMap
value set to enabled or disabled, the .largerText
and .hearingDevice
feature do offer specific values:
LargerText
iOS
- XS
- S
- M (default)
- L
- XL
- XXL
- XXXL
- Accessibility M
- Accessibility L
- Accessibility XL
- Accessibility XXL
- Accessibility XXXL
- Unknown
watchOS
- XS
- S (default watch with 38mm)
- L (default watch with 42mm)
- XL
- XXL
- XXXL
- Unknown
HearingDevice
- both
- left
- right
- disabled