2026年のiOSアクセシビリティ面接質問: VoiceOverとDynamic Type

iOS面接に向け、VoiceOver、Dynamic Type、セマンティックtraits、監査などアクセシビリティの重要質問を解説します。

iOSアクセシビリティ面接質問: VoiceOverとDynamic Type

アクセシビリティはiOS開発で欠かせないスキルになりました。採用担当者はVoiceOver、Dynamic Type、アクセシビリティAPIの習熟度を体系的に評価しています。本記事では技術面接で成功するための主要な概念をまとめます。

なぜアクセシビリティが面接で重要か

世界には10億人以上の障がいを持つ人がいます。Appleは厳格なアクセシビリティ基準を課しており、多くの企業はアクセシブルでないアプリの公開を拒みます。このスキルはシニア候補者を他から差別化します。

iOSアクセシビリティの基礎

1. iOSの主なアクセシビリティツールは?

iOSはさまざまな障がいに向けた包括的なアクセシビリティツール群を提供します。VoiceOverは視覚に障がいを持つユーザーへ画面読み上げを行います。Dynamic Typeは文字サイズを調整します。Switch Controlは外部スイッチでの操作を可能にします。Voice Controlは完全な音声操作を提供します。

AccessibilityTools.swiftswift
// Check if VoiceOver is running
if UIAccessibility.isVoiceOverRunning {
    // Adapt the interface for VoiceOver
    showSimplifiedInterface()
}

// Check if Reduce Motion is enabled
if UIAccessibility.isReduceMotionEnabled {
    // Disable complex animations
    animationDuration = 0
}

// Check if Bold Text is enabled
if UIAccessibility.isBoldTextEnabled {
    // Use heavier fonts
    applyBoldFonts()
}

// Observe settings changes
NotificationCenter.default.addObserver(
    forName: UIAccessibility.voiceOverStatusDidChangeNotification,
    object: nil,
    queue: .main
) { _ in
    // React to the change
    updateAccessibilityLayout()
}

これらのAPIにより、ユーザー設定や有効な支援技術に応じて画面を動的に最適化できます。

2. VoiceOverは技術的にどう動くのか?

VoiceOverはインターフェースを巡回してアクセシブルな要素を識別します。各要素はlabel、value、traits、actionsといったプロパティを公開します。スクリーンリーダーはユーザーがジェスチャで操作する間、これらの情報を音声で合成します。

VoiceOverBasics.swiftswift
// In UIKit: configure an accessible element
class CustomButton: UIButton {
    override var accessibilityLabel: String? {
        get { "Validation button" }  // What VoiceOver reads
        set { }
    }

    override var accessibilityHint: String? {
        get { "Double-tap to confirm the order" }  // Possible action
        set { }
    }

    override var accessibilityTraits: UIAccessibilityTraits {
        get { .button }  // Element type
        set { }
    }

    override var isAccessibilityElement: Bool {
        get { true }  // Element focusable by VoiceOver
        set { }
    }
}

// In SwiftUI: equivalent with modifiers
struct ValidateButton: View {
    var body: some View {
        Button("Validate") {
            confirmOrder()
        }
        .accessibilityLabel("Validation button")
        .accessibilityHint("Double-tap to confirm the order")
    }
}

アクセシビリティの階層は視覚的な階層と異なる場合があり、複雑なレイアウトでも論理的なナビゲーションを可能にします。

3. accessibilityLabelとaccessibilityValueの違いは?

accessibilityLabelは要素を恒常的に識別し、accessibilityValueは変化しうる現在の状態を表現します。スライダーやスイッチのような動的なコントロールではこの区別が重要です。

LabelVsValue.swiftswift
// Example with a volume slider
class VolumeSlider: UISlider {
    override var accessibilityLabel: String? {
        get { "Volume" }  // Fixed element identity
        set { }
    }

    override var accessibilityValue: String? {
        get { "\(Int(value * 100)) percent" }  // Current state that changes
        set { }
    }
}
// VoiceOver reads: "Volume, 75 percent"

// In SwiftUI with a Toggle
struct NotificationToggle: View {
    @Binding var isEnabled: Bool

    var body: some View {
        Toggle(isOn: $isEnabled) {
            Text("Notifications")
        }
        .accessibilityLabel("Push notifications")
        // accessibilityValue is automatic for Toggle: "on" or "off"
    }
}

// For a custom stepper
struct QuantityStepper: View {
    @State private var quantity = 1

    var body: some View {
        Stepper("Quantity", value: $quantity, in: 1...99)
            .accessibilityLabel("Item quantity")
            .accessibilityValue("\(quantity) item\(quantity > 1 ? "s" : "")")
    }
}

VoiceOverは常にlabelに続いてvalueを読み上げ、ユーザーは要素自体と現状の両方を把握できます。

4. accessibilityTraitsとその重要性を説明してください

traitsはVoiceOverに要素の種類と振る舞いを伝えます。読み上げ方や利用可能なアクションを左右します。.buttonのtraitがないボタンはボタンとしてアナウンスされません。

AccessibilityTraits.swiftswift
// Common traits in UIKit
class CustomCell: UITableViewCell {
    override var accessibilityTraits: UIAccessibilityTraits {
        get {
            var traits: UIAccessibilityTraits = []

            // Element type
            traits.insert(.button)           // Interactive element

            // Current state
            if isSelected {
                traits.insert(.selected)     // Currently selected
            }

            // Special characteristics
            if !isEnabled {
                traits.insert(.notEnabled)   // Disabled
            }

            return traits
        }
        set { }
    }
}

// Available traits
// .button         - Clickable element
// .link           - Hyperlink
// .header         - Section header
// .image          - Image
// .selected       - Selected state
// .notEnabled     - Disabled
// .adjustable     - Adjustable value (slider)
// .staticText     - Non-interactive text
// .searchField    - Search field
// .playsSound     - Plays a sound
// .startsMediaSession - Starts media playback

// In SwiftUI
struct SectionHeader: View {
    let title: String

    var body: some View {
        Text(title)
            .font(.headline)
            .accessibilityAddTraits(.isHeader)  // Announced as header
    }
}

struct SelectableRow: View {
    let isSelected: Bool

    var body: some View {
        HStack {
            Text("Option")
            if isSelected {
                Image(systemName: "checkmark")
            }
        }
        .accessibilityElement(children: .combine)
        .accessibilityAddTraits(isSelected ? .isSelected : [])
    }
}

traitsにより支援技術はユーザーへ一貫した予測可能な体験を提供できます。

Dynamic Typeと適応的な文字サイズ

5. Dynamic Typeを正しく実装するには?

Dynamic Typeはユーザーがシステムの文字サイズを調整できる機能です。意味的なテキストスタイルと柔軟な制約を使い、画面が自動的に適応するよう実装します。

DynamicType.swiftswift
// In UIKit: use preferred text styles
class ArticleCell: UITableViewCell {
    let titleLabel = UILabel()
    let bodyLabel = UILabel()

    func configure() {
        // Use semantic text styles
        titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        bodyLabel.font = UIFont.preferredFont(forTextStyle: .body)

        // CRUCIAL: enable automatic adjustment
        titleLabel.adjustsFontForContentSizeCategory = true
        bodyLabel.adjustsFontForContentSizeCategory = true

        // Allow multiline for large sizes
        titleLabel.numberOfLines = 0
        bodyLabel.numberOfLines = 0
    }
}

// In SwiftUI: it's automatic with styles
struct ArticleView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Article Title")
                .font(.headline)  // Adapts automatically

            Text("Article content...")
                .font(.body)
        }
    }
}

// Custom font with Dynamic Type
extension UIFont {
    static func customFont(
        size: CGFloat,
        style: TextStyle
    ) -> UIFont {
        let customFont = UIFont(name: "CustomFont-Regular", size: size)!

        // Adapt to current size category
        return UIFontMetrics(forTextStyle: style)
            .scaledFont(for: customFont)
    }
}

adjustsFontForContentSizeCategoryを有効にしないとユーザーが設定を変更してもlabelは初期サイズのままです。

6. 極端なアクセシビリティサイズへの対応は?

アクセシビリティサイズ(AX1からAX5)は文字を3倍ほどに拡大できます。インターフェースは別のレイアウトで対応すべきです: 横並びを縦並びに変更し、装飾要素を非表示にし、余白を調整します。

LargeContentSize.swiftswift
// Detect accessibility sizes
struct AdaptiveLayout: View {
    @Environment(\.dynamicTypeSize) var dynamicTypeSize

    var body: some View {
        // Different layout for large sizes
        if dynamicTypeSize.isAccessibilitySize {
            // Vertical layout for large sizes
            VStack(alignment: .leading, spacing: 12) {
                iconAndTitle
                actionButtons
            }
        } else {
            // Standard horizontal layout
            HStack {
                iconAndTitle
                Spacer()
                actionButtons
            }
        }
    }

    var iconAndTitle: some View {
        HStack {
            Image(systemName: "bell.fill")
                .accessibilityHidden(true)  // Decorative, ignore
            Text("Notifications")
        }
    }

    var actionButtons: some View {
        HStack(spacing: 16) {
            Button("Enable") { }
            Button("Settings") { }
        }
    }
}

// In UIKit: observe changes
class AdaptiveViewController: UIViewController {
    override func traitCollectionDidChange(
        _ previousTraitCollection: UITraitCollection?
    ) {
        super.traitCollectionDidChange(previousTraitCollection)

        // Check if size category changed
        if traitCollection.preferredContentSizeCategory !=
           previousTraitCollection?.preferredContentSizeCategory {
            updateLayoutForContentSize()
        }
    }

    func updateLayoutForContentSize() {
        let category = traitCollection.preferredContentSizeCategory

        if category.isAccessibilityCategory {
            // Enable accessible layout
            stackView.axis = .vertical
            decorativeView.isHidden = true
        } else {
            // Standard layout
            stackView.axis = .horizontal
            decorativeView.isHidden = false
        }
    }
}

アクセシビリティカテゴリは.accessibilityMedium(AX1)から始まります。これらのケースを最初から見越して設計することが必要です。

iOSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

7. 画像をアクセシブルにする方法は?

画像は役割により扱いが異なります。情報を伝える画像は説明が必要、装飾画像は無視するべき、操作可能な画像にはアクションのlabelが必要です。

AccessibleImages.swiftswift
// Informative image: describe the content
struct ProductImage: View {
    let product: Product

    var body: some View {
        AsyncImage(url: product.imageURL) { image in
            image
                .resizable()
                .accessibilityLabel(product.imageDescription)
                // "Product photo: iPhone 15 Pro, titanium color"
        } placeholder: {
            ProgressView()
                .accessibilityLabel("Loading image")
        }
    }
}

// Decorative image: hide from VoiceOver
struct DecorationView: View {
    var body: some View {
        Image(systemName: "sparkles")
            .accessibilityHidden(true)  // Ignored by VoiceOver
    }
}

// Image button: describe the action, not the image
struct FavoriteButton: View {
    @Binding var isFavorite: Bool

    var body: some View {
        Button {
            isFavorite.toggle()
        } label: {
            Image(systemName: isFavorite ? "heart.fill" : "heart")
        }
        // Don't describe the icon, describe the action
        .accessibilityLabel(isFavorite ? "Remove from favorites" : "Add to favorites")
    }
}

// In UIKit
class ProductImageView: UIImageView {
    override var isAccessibilityElement: Bool {
        get { true }
        set { }
    }

    func configure(with product: Product) {
        image = product.image
        accessibilityLabel = "Photo: \(product.name), \(product.color)"
    }
}

// Image with embedded text (infographic)
class InfographicView: UIImageView {
    override var accessibilityLabel: String? {
        get {
            // Transcribe the textual content of the infographic
            """
            Statistics infographic 2026.
            Growth: 45%.
            Active users: 2.3 million.
            Satisfaction: 4.8 out of 5.
            """
        }
        set { }
    }
}

説明の不適切な画像や非表示にされていない装飾画像はVoiceOverの体験を大きく損ないます。

グルーピングと高度なナビゲーション

8. VoiceOver向けに要素をグループ化するには?

グループ化は複数の視覚要素を1つのアクセシブル要素にまとめます。これによりナビゲーションが速くなり、1回のアナウンスで完全な文脈を提供できます。

AccessibilityGrouping.swiftswift
// In SwiftUI: combine children
struct ProductCard: View {
    let product: Product

    var body: some View {
        VStack(alignment: .leading) {
            Text(product.name)
                .font(.headline)
            Text(product.price)
                .font(.subheadline)
            HStack {
                Image(systemName: "star.fill")
                Text(product.rating)
            }
        }
        // Combines all text into a single element
        .accessibilityElement(children: .combine)
        // VoiceOver reads: "iPhone 15 Pro, $1199, 4.8 stars"
    }
}

// In SwiftUI: ignore children and define manually
struct OrderSummary: View {
    let order: Order

    var body: some View {
        HStack {
            Image(systemName: "bag")
            VStack(alignment: .leading) {
                Text(order.itemCount)
                Text(order.total)
            }
            Image(systemName: "chevron.right")
        }
        // Ignore children hierarchy
        .accessibilityElement(children: .ignore)
        // Define custom label
        .accessibilityLabel("Order of \(order.itemCount) items for \(order.total)")
        .accessibilityHint("Double-tap to view details")
        .accessibilityAddTraits(.isButton)
    }
}

// In UIKit: accessible container
class ProductCardView: UIView {
    let nameLabel = UILabel()
    let priceLabel = UILabel()
    let ratingLabel = UILabel()

    override var isAccessibilityElement: Bool {
        get { true }  // The container is the accessible element
        set { }
    }

    override var accessibilityLabel: String? {
        get {
            // Combine information
            "\(nameLabel.text ?? ""), \(priceLabel.text ?? ""), \(ratingLabel.text ?? "")"
        }
        set { }
    }
}

グループ化しない場合、VoiceOverはTextごとに止まりナビゲーションが大幅に遅くなります。

9. カスタムのアクセシビリティアクションを実装するには?

カスタムアクションは視覚的なUIを煩雑にせずアクセシブルな機能を追加します。VoiceOverユーザーは縦方向のスワイプで利用します。

CustomAccessibilityActions.swiftswift
// In SwiftUI: custom actions
struct EmailRow: View {
    let email: Email
    let onArchive: () -> Void
    let onDelete: () -> Void
    let onFlag: () -> Void

    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(email.sender)
                    .font(.headline)
                Text(email.subject)
                    .font(.subheadline)
            }
            Spacer()
            if email.isFlagged {
                Image(systemName: "flag.fill")
            }
        }
        .accessibilityElement(children: .combine)
        .accessibilityLabel("\(email.sender), \(email.subject)")
        // Actions accessible via vertical swipe
        .accessibilityAction(named: "Archive") {
            onArchive()
        }
        .accessibilityAction(named: "Delete") {
            onDelete()
        }
        .accessibilityAction(named: email.isFlagged ? "Remove flag" : "Flag") {
            onFlag()
        }
    }
}

// In UIKit: override accessibilityCustomActions
class EmailCell: UITableViewCell {
    var email: Email!
    var onArchive: (() -> Void)?
    var onDelete: (() -> Void)?

    override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
        get {
            [
                UIAccessibilityCustomAction(
                    name: "Archive",
                    target: self,
                    selector: #selector(archiveAction)
                ),
                UIAccessibilityCustomAction(
                    name: "Delete",
                    target: self,
                    selector: #selector(deleteAction)
                )
            ]
        }
        set { }
    }

    @objc private func archiveAction() -> Bool {
        onArchive?()
        return true  // true = action succeeded
    }

    @objc private func deleteAction() -> Bool {
        onDelete?()
        return true
    }
}

カスタムアクションはVoiceOverで発見しづらいスワイプジェスチャの代わりに有効です。

10. VoiceOverのフォーカスをプログラムで制御するには?

UIの変化(モーダルの表示、コンテンツの読み込み、画面遷移)後、フォーカスを制御することは重要です。フォーカスはユーザーを必要な情報へ導くべきです。

AccessibilityFocus.swiftswift
// In SwiftUI with @AccessibilityFocusState (iOS 15+)
struct SearchView: View {
    @State private var searchText = ""
    @State private var results: [Result] = []
    @AccessibilityFocusState private var focusedResult: Result?

    var body: some View {
        VStack {
            TextField("Search", text: $searchText)
                .onSubmit {
                    performSearch()
                }

            List(results) { result in
                ResultRow(result: result)
                    .accessibilityFocused($focusedResult, equals: result)
            }
        }
    }

    func performSearch() {
        Task {
            results = await api.search(searchText)
            // Move focus to first result
            if let first = results.first {
                focusedResult = first
            }
        }
    }
}

// In UIKit: post a notification
class ModalViewController: UIViewController {
    @IBOutlet weak var titleLabel: UILabel!

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        // Announce and move focus
        UIAccessibility.post(
            notification: .screenChanged,  // New screen
            argument: titleLabel           // Element to focus
        )
    }

    func showError(_ message: String) {
        errorLabel.text = message
        errorLabel.isHidden = false

        // Announce layout change
        UIAccessibility.post(
            notification: .layoutChanged,  // Change in current screen
            argument: errorLabel
        )
    }

    func showLoadingComplete() {
        // Announce without moving focus
        UIAccessibility.post(
            notification: .announcement,
            argument: "Loading complete"
        )
    }
}

.screenChanged.layoutChangedの通知はUIの重大な変更をVoiceOverへ伝えます。

iOSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

調整可能な要素と複雑なコントロール

11. カスタムスライダーをアクセシブルにするには?

カスタムスライダーは.adjustableのtraitを実装し、増減のジェスチャに応答します。VoiceOverは縦スワイプで値を変更します。

AccessibleSlider.swiftswift
// In SwiftUI: Slider is accessible by default
struct VolumeControl: View {
    @Binding var volume: Double

    var body: some View {
        Slider(value: $volume, in: 0...100, step: 5)
            .accessibilityLabel("Volume")
            .accessibilityValue("\(Int(volume)) percent")
    }
}

// Custom accessible slider
struct CustomRatingSlider: View {
    @Binding var rating: Int

    var body: some View {
        HStack {
            ForEach(1...5, id: \.self) { star in
                Image(systemName: star <= rating ? "star.fill" : "star")
                    .onTapGesture { rating = star }
            }
        }
        .accessibilityElement(children: .ignore)
        .accessibilityLabel("Rating")
        .accessibilityValue("\(rating) star\(rating > 1 ? "s" : "") out of 5")
        .accessibilityAdjustableAction { direction in
            switch direction {
            case .increment:
                rating = min(5, rating + 1)
            case .decrement:
                rating = max(1, rating - 1)
            @unknown default:
                break
            }
        }
    }
}

// In UIKit: adjustable trait
class StarRatingView: UIView {
    var rating: Int = 3 {
        didSet {
            updateStars()
            // Announce the new value
            UIAccessibility.post(
                notification: .announcement,
                argument: accessibilityValue
            )
        }
    }

    override var accessibilityTraits: UIAccessibilityTraits {
        get { .adjustable }  // Enables adjustment gestures
        set { }
    }

    override var accessibilityLabel: String? {
        get { "Rating" }
        set { }
    }

    override var accessibilityValue: String? {
        get { "\(rating) stars out of 5" }
        set { }
    }

    // Called on swipe up
    override func accessibilityIncrement() {
        rating = min(5, rating + 1)
    }

    // Called on swipe down
    override func accessibilityDecrement() {
        rating = max(1, rating - 1)
    }
}

.adjustableがないと、カスタムスライダーは星を一つずつタップさせることになります。

12. アクセシブルなカルーセルの作り方は?

カルーセルはアクセシビリティ上の課題があります。非線形のナビゲーション、画面外のコンテンツ、複数の状態などです。.adjustableのtraitと文脈に応じたアナウンスを組み合わせます。

AccessibleCarousel.swiftswift
struct ImageCarousel: View {
    let images: [CarouselImage]
    @State private var currentIndex = 0

    var body: some View {
        TabView(selection: $currentIndex) {
            ForEach(images.indices, id: \.self) { index in
                CarouselItem(image: images[index])
                    .tag(index)
            }
        }
        .tabViewStyle(.page)
        .accessibilityElement(children: .contain)
        .accessibilityLabel("Image carousel")
        .accessibilityValue("Image \(currentIndex + 1) of \(images.count)")
        .accessibilityHint("Swipe to change image")
        .accessibilityAdjustableAction { direction in
            withAnimation {
                switch direction {
                case .increment:
                    currentIndex = min(images.count - 1, currentIndex + 1)
                case .decrement:
                    currentIndex = max(0, currentIndex - 1)
                @unknown default:
                    break
                }
            }
        }
    }
}

// In UIKit with UIPageViewController
class CarouselAccessibilityContainer: UIView {
    var currentPage = 0
    var totalPages = 5

    override var isAccessibilityElement: Bool {
        get { true }
        set { }
    }

    override var accessibilityTraits: UIAccessibilityTraits {
        get { .adjustable }
        set { }
    }

    override var accessibilityLabel: String? {
        get { "Promotional carousel" }
        set { }
    }

    override var accessibilityValue: String? {
        get { "Page \(currentPage + 1) of \(totalPages)" }
        set { }
    }

    override func accessibilityIncrement() {
        guard currentPage < totalPages - 1 else { return }
        currentPage += 1
        delegate?.scrollToPage(currentPage)
    }

    override func accessibilityDecrement() {
        guard currentPage > 0 else { return }
        currentPage -= 1
        delegate?.scrollToPage(currentPage)
    }
}

この方法であれば、標準のスワイプジェスチャに依存せずにカルーセルを操作できます。

13. フォームのアクセシビリティはどう扱う?

アクセシブルなフォームでは各フィールドにlabelを関連付け、エラーを明確に示し、フィールド間をスムーズに移動できるようにします。

AccessibleForms.swiftswift
struct RegistrationForm: View {
    @State private var email = ""
    @State private var password = ""
    @State private var emailError: String?
    @State private var passwordError: String?

    var body: some View {
        Form {
            Section {
                TextField("Email", text: $email)
                    .textContentType(.emailAddress)
                    .keyboardType(.emailAddress)
                    .accessibilityLabel("Email address")
                    .accessibilityHint(emailError != nil ? "Error: \(emailError!)" : nil)

                if let error = emailError {
                    Text(error)
                        .foregroundColor(.red)
                        .font(.caption)
                        .accessibilityLabel("Email error: \(error)")
                }
            }

            Section {
                SecureField("Password", text: $password)
                    .textContentType(.newPassword)
                    .accessibilityLabel("Password")
                    .accessibilityHint(passwordHint)

                PasswordStrengthIndicator(password: password)
            }

            Button("Create account") {
                submitForm()
            }
            .disabled(!isFormValid)
            .accessibilityHint(isFormValid ? "Double-tap to create your account" : "Form incomplete")
        }
    }

    var passwordHint: String {
        if let error = passwordError {
            return "Error: \(error)"
        }
        return "Minimum 8 characters with uppercase and numbers"
    }
}

// Accessible password strength indicator
struct PasswordStrengthIndicator: View {
    let password: String

    var strength: PasswordStrength {
        // Calculation logic
        PasswordStrength.calculate(password)
    }

    var body: some View {
        HStack(spacing: 4) {
            ForEach(0..<4) { index in
                Rectangle()
                    .fill(index < strength.level ? strength.color : Color.gray.opacity(0.3))
                    .frame(height: 4)
            }
        }
        .accessibilityElement(children: .ignore)
        .accessibilityLabel("Password strength")
        .accessibilityValue(strength.description)  // "Weak", "Medium", "Strong", "Very strong"
    }
}

アクセシビリティのhintは視覚UIを汚さずにエラーや指示を伝えます。

アクセシビリティ監査とテスト

14. iOSアプリのアクセシビリティを監査する方法は?

監査は自動ツールと手動テストの組み合わせです。XcodeのAccessibility Inspectorで要素を分析し、VoiceOverで実体験を検証し、ユニットテストでアクセシビリティのプロパティを確認します。

AccessibilityAudit.swiftswift
// Unit test accessibility properties
import XCTest
@testable import MyApp

class AccessibilityTests: XCTestCase {

    func testProductCardAccessibility() {
        let product = Product(name: "iPhone", price: "$999", rating: "4.5")
        let view = ProductCardView(product: product)

        // Verify element is accessible
        XCTAssertTrue(view.isAccessibilityElement)

        // Verify label
        XCTAssertEqual(
            view.accessibilityLabel,
            "iPhone, $999, 4.5 stars"
        )

        // Verify traits
        XCTAssertTrue(view.accessibilityTraits.contains(.button))
    }

    func testDynamicTypeSupport() {
        let label = UILabel()
        label.font = UIFont.preferredFont(forTextStyle: .body)

        // Verify automatic adjustment
        XCTAssertTrue(label.adjustsFontForContentSizeCategory)
    }
}

// UI test with simulated VoiceOver
class AccessibilityUITests: XCTestCase {

    func testLoginFlowAccessibility() {
        let app = XCUIApplication()
        app.launch()

        // Access elements by accessibility label
        let emailField = app.textFields["Email address"]
        XCTAssertTrue(emailField.exists)

        emailField.tap()
        emailField.typeText("test@example.com")

        let passwordField = app.secureTextFields["Password"]
        XCTAssertTrue(passwordField.exists)

        let loginButton = app.buttons["Sign in"]
        XCTAssertTrue(loginButton.exists)
        XCTAssertTrue(loginButton.isEnabled)
    }

    // Automatic audit iOS 17+
    func testAccessibilityAudit() throws {
        let app = XCUIApplication()
        app.launch()

        // Automatic screen audit
        try app.performAccessibilityAudit()
    }
}

iOS 17以降の自動監査はlabelの欠落、コントラスト不足、要素の小ささなど一般的な問題を検出します。

15. もっとも多いアクセシビリティの誤りは?

頻発する誤りはlabelの欠落、未記述の画像、Dynamic Type非対応、適切なtraitのない操作要素などです。

CommonMistakes.swiftswift
// WRONG: Button without accessible label
Button {
    performAction()
} label: {
    Image(systemName: "plus")
}
// VoiceOver reads: "Button" - useless

// CORRECT: Explicit label
Button {
    performAction()
} label: {
    Image(systemName: "plus")
}
.accessibilityLabel("Add item")


// WRONG: Fixed font size
Text("Important")
    .font(.system(size: 16))  // Doesn't respect Dynamic Type

// CORRECT: Semantic text style
Text("Important")
    .font(.body)  // Adapts to preferences


// WRONG: Informative image without description
Image("product-photo")

// CORRECT: Content description
Image("product-photo")
    .accessibilityLabel("Product photo: Black audio headphones")


// WRONG: Clickable element without button trait
Text("Learn more")
    .onTapGesture { showDetails() }
// VoiceOver doesn't know it's interactive

// CORRECT: Appropriate traits
Text("Learn more")
    .onTapGesture { showDetails() }
    .accessibilityAddTraits(.isButton)
    .accessibilityHint("Double-tap to show details")


// WRONG: Insufficient contrast
Text("Light gray text")
    .foregroundColor(.gray.opacity(0.4))

// CORRECT: Minimum contrast ratio 4.5:1
Text("Readable text")
    .foregroundColor(.secondary)  // Respects guidelines


// WRONG: Touch target too small
Button { } label: {
    Image(systemName: "xmark")
        .font(.caption)  // Too small (< 44x44 points)
}

// CORRECT: Minimum 44x44 point area
Button { } label: {
    Image(systemName: "xmark")
}
.frame(minWidth: 44, minHeight: 44)

これらの誤りはAccessibility Inspectorで検出でき、わずか数行で修正可能です。

まとめ

iOSのアクセシビリティは、視覚障がいのあるユーザー向けのVoiceOver、テキスト適応のためのDynamic Type、一貫したナビゲーションのための意味的traitsという3つの柱で支えられます。これらを習得することは、企業が積極的に求める専門性の証となります。

チェックリスト

  • VoiceOverに適切なlabel、hint、traitsを設定する
  • 意味的なテキストスタイルでDynamic Typeを実装する
  • 極端なアクセシビリティサイズに合わせたレイアウトを用意する
  • 要素をグループ化して効率的なナビゲーションを実現する
  • UI変更後はフォーカスをプログラムで制御する
  • カスタムコントロールを調整可能にする
  • Accessibility Inspectorと自動テストで定期的に監査する
  • 頻発する誤りを避ける: labelの欠落、未記述の画像、コントラスト不足

補足リソース

Appleの公式ドキュメント"Human Interface Guidelines - Accessibility"は引き続き重要な参考資料です。VoiceOverを有効にした定期的な動作確認は自動ツールでは見つけられない問題の発見に役立ちます。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#ios
#accessibility
#voiceover
#dynamic-type
#swift

共有

関連記事