6.7 KiB
6.7 KiB
Accessibility
iOS accessibility guide covering Dynamic Type, semantic colors, VoiceOver, and motion adaptation.
Dynamic Type
Using System Fonts
private func setupLabels() {
let titleLabel = UILabel()
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontForContentSizeCategory = true
let bodyLabel = UILabel()
bodyLabel.font = .preferredFont(forTextStyle: .body)
bodyLabel.adjustsFontForContentSizeCategory = true
bodyLabel.numberOfLines = 0
}
Custom Font Scaling
extension UIFont {
static func scaled(_ name: String, size: CGFloat, for style: TextStyle) -> UIFont {
guard let font = UIFont(name: name, size: size) else {
return .preferredFont(forTextStyle: style)
}
return UIFontMetrics(forTextStyle: style).scaledFont(for: font)
}
}
let customFont = UIFont.scaled("Avenir-Medium", size: 16, for: .body)
Text Style Reference
| Style | Default Size | Usage |
|---|---|---|
.largeTitle |
34pt | Screen titles |
.title1 |
28pt | Primary headings |
.title2 |
22pt | Secondary headings |
.title3 |
20pt | Tertiary headings |
.headline |
17pt (semibold) | Important information |
.body |
17pt | Body text |
.callout |
16pt | Explanatory text |
.subheadline |
15pt | Subtitles |
.footnote |
13pt | Footnotes |
.caption1 |
12pt | Labels |
.caption2 |
11pt | Small labels |
Adapting Layout for Large Text
override func traitCollectionDidChange(_ previous: UITraitCollection?) {
super.traitCollectionDidChange(previous)
let isLargeText = traitCollection.preferredContentSizeCategory.isAccessibilityCategory
contentStack.axis = isLargeText ? .vertical : .horizontal
if isLargeText {
iconImageView.snp.remakeConstraints { make in
make.size.equalTo(64)
}
} else {
iconImageView.snp.remakeConstraints { make in
make.size.equalTo(44)
}
}
}
Semantic Colors
Use system semantic colors for automatic Dark Mode adaptation:
view.backgroundColor = .systemBackground
containerView.backgroundColor = .secondarySystemBackground
cardView.backgroundColor = .tertiarySystemBackground
titleLabel.textColor = .label
subtitleLabel.textColor = .secondaryLabel
hintLabel.textColor = .tertiaryLabel
placeholderLabel.textColor = .placeholderText
separatorView.backgroundColor = .separator
borderView.layer.borderColor = UIColor.separator.cgColor
System Color Reference
| Color | Light Mode | Dark Mode | Usage |
|---|---|---|---|
.systemBackground |
White | Black | Main background |
.secondarySystemBackground |
Light gray | Dark gray | Card/grouped background |
.tertiarySystemBackground |
Lighter gray | Medium gray | Nested content background |
.label |
Black | White | Primary text |
.secondaryLabel |
Gray | Light gray | Secondary text |
.tertiaryLabel |
Light gray | Dark gray | Auxiliary text |
Custom Color Adaptation
extension UIColor {
static let customAccent = UIColor { traitCollection in
switch traitCollection.userInterfaceStyle {
case .dark:
return UIColor(red: 0.4, green: 0.8, blue: 1.0, alpha: 1.0)
default:
return UIColor(red: 0.0, green: 0.5, blue: 0.8, alpha: 1.0)
}
}
}
VoiceOver
Basic Labels
let cartButton = UIButton(type: .system)
cartButton.setImage(UIImage(systemName: "cart.badge.plus"), for: .normal)
cartButton.accessibilityLabel = "Add to cart"
let ratingView = UIView()
ratingView.accessibilityLabel = "Rating: 4 out of 5 stars"
let closeButton = UIButton()
closeButton.accessibilityLabel = "Close"
closeButton.accessibilityHint = "Dismisses this dialog"
Custom Accessibility
class ProductCell: UICollectionViewCell {
override var accessibilityLabel: String? {
get {
return "\(product.name), \(product.price), \(product.isAvailable ? "In stock" : "Out of stock")"
}
set {}
}
override var accessibilityTraits: UIAccessibilityTraits {
get {
var traits: UIAccessibilityTraits = .button
if product.isSelected {
traits.insert(.selected)
}
return traits
}
set {}
}
}
Accessibility Container
class CustomContainerView: UIView {
override var isAccessibilityElement: Bool {
get { false }
set {}
}
override var accessibilityElements: [Any]? {
get {
return [titleLabel, actionButton, detailLabel]
}
set {}
}
}
VoiceOver Notifications
func didLoadContent() {
UIAccessibility.post(notification: .screenChanged, argument: headerLabel)
}
func didUpdateStatus() {
UIAccessibility.post(notification: .announcement, argument: "Download complete")
}
Reduce Motion
func animateTransition() {
let duration: TimeInterval = UIAccessibility.isReduceMotionEnabled ? 0 : 0.3
UIView.animate(withDuration: duration) {
self.cardView.alpha = 1
}
}
func showPopup() {
if UIAccessibility.isReduceMotionEnabled {
popupView.alpha = 1
} else {
popupView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
popupView.alpha = 0
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0) {
self.popupView.transform = .identity
self.popupView.alpha = 1
}
}
}
Observing Setting Changes
NotificationCenter.default.addObserver(
self,
selector: #selector(reduceMotionChanged),
name: UIAccessibility.reduceMotionStatusDidChangeNotification,
object: nil
)
@objc func reduceMotionChanged() {
updateAnimationSettings()
}
Accessibility Checklist
Basic Requirements
- All icon buttons have
accessibilityLabel - Custom controls have correct
accessibilityTraits - Images have
accessibilityLabelor marked as decorative - Forms have clear error messages
Dynamic Type
- Using
preferredFont(forTextStyle:) - Set
adjustsFontForContentSizeCategory = true - Layout adapts at accessibility sizes
- Text is not truncated
Color Contrast
- Body text contrast >= 4.5:1
- Large text contrast >= 3:1
- Information not conveyed by color alone
Motion
- Respect Reduce Motion setting
- No flashing or rapid animation
- Auto-playing animations can be paused
Interaction
- Touch targets >= 44x44pt
- Gestures have alternative actions
- Timeouts can be extended
UIKit, VoiceOver, Dynamic Type, and Apple are trademarks of Apple Inc.