260 lines
6.7 KiB
Markdown
260 lines
6.7 KiB
Markdown
# Accessibility
|
|
|
|
iOS accessibility guide covering Dynamic Type, semantic colors, VoiceOver, and motion adaptation.
|
|
|
|
## Dynamic Type
|
|
|
|
### Using System Fonts
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
class CustomContainerView: UIView {
|
|
override var isAccessibilityElement: Bool {
|
|
get { false }
|
|
set {}
|
|
}
|
|
|
|
override var accessibilityElements: [Any]? {
|
|
get {
|
|
return [titleLabel, actionButton, detailLabel]
|
|
}
|
|
set {}
|
|
}
|
|
}
|
|
```
|
|
|
|
### VoiceOver Notifications
|
|
|
|
```swift
|
|
func didLoadContent() {
|
|
UIAccessibility.post(notification: .screenChanged, argument: headerLabel)
|
|
}
|
|
|
|
func didUpdateStatus() {
|
|
UIAccessibility.post(notification: .announcement, argument: "Download complete")
|
|
}
|
|
```
|
|
|
|
## Reduce Motion
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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 `accessibilityLabel` or 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.*
|