Domande di colloquio sull'accessibilità iOS nel 2026: VoiceOver e Dynamic Type

Preparati ai colloqui iOS con domande chiave sull'accessibilità: VoiceOver, Dynamic Type, trait semantici e audit.

Domande di colloquio sull'accessibilità iOS: VoiceOver e Dynamic Type

L'accessibilità è diventata una competenza critica nello sviluppo iOS. I recruiter ora valutano sistematicamente la padronanza di VoiceOver, Dynamic Type e delle API di accessibilità. Queste domande coprono i concetti essenziali per riuscire nei colloqui tecnici.

Perché l'accessibilità conta nei colloqui

Oltre 1 miliardo di persone vive con una disabilità. Apple impone standard di accessibilità rigorosi e molte aziende rifiutano di pubblicare app non accessibili. Questa competenza distingue i candidati senior dagli altri.

Fondamenti dell'accessibilità iOS

1. Quali sono i principali strumenti di accessibilità di iOS?

iOS offre un ecosistema completo di strumenti di accessibilità rivolti a diversi tipi di disabilità. VoiceOver consente la lettura dello schermo per persone con disabilità visive. Dynamic Type adatta la dimensione del testo. Switch Control permette il controllo tramite interruttori esterni. Voice Control offre il controllo vocale completo.

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()
}

Queste API permettono di adattare dinamicamente l'interfaccia in base alle preferenze dell'utente e alle tecnologie assistive attive.

2. Come funziona VoiceOver tecnicamente?

VoiceOver attraversa l'interfaccia identificando gli elementi accessibili. Ogni elemento espone proprietà come label, value, trait e azioni. Lo screen reader sintetizza vocalmente queste informazioni mentre l'utente naviga con i gesti.

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")
    }
}

La gerarchia di accessibilità può differire dalla gerarchia visiva, consentendo una navigazione logica anche con layout complessi.

3. Qual è la differenza tra accessibilityLabel e accessibilityValue?

L'accessibilityLabel identifica permanentemente l'elemento, mentre accessibilityValue rappresenta il suo stato attuale, che può cambiare. Questa distinzione è cruciale per controlli dinamici come slider o switch.

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 annuncia sempre la label seguita dal value, permettendo all'utente di capire sia cos'è l'elemento sia il suo stato attuale.

4. Spiega accessibilityTraits e la loro importanza

I trait informano VoiceOver sul tipo e sul comportamento di un elemento. Determinano come l'elemento viene annunciato e quali azioni sono disponibili. Un pulsante senza il trait .button non sarà annunciato come tale.

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 : [])
    }
}

I trait permettono alle tecnologie assistive di offrire un'esperienza coerente e prevedibile per gli utenti.

Dynamic Type e dimensioni di testo adattive

5. Come si implementa correttamente Dynamic Type?

Dynamic Type permette all'utente di regolare la dimensione del testo di sistema. Un'implementazione corretta usa stili di testo semantici e vincoli flessibili affinché l'interfaccia si adatti automaticamente.

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)
    }
}

Senza adjustsFontForContentSizeCategory, le label mantengono la loro dimensione iniziale anche quando l'utente cambia le preferenze.

6. Come gestire le dimensioni di accessibilità estreme?

Le dimensioni di accessibilità (da AX1 ad AX5) possono triplicare la dimensione del testo. L'interfaccia deve adattarsi con layout alternativi: impilare verticalmente invece che orizzontalmente, nascondere gli elementi decorativi e regolare gli spazi.

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
        }
    }
}

Le categorie di accessibilità iniziano da .accessibilityMedium (AX1). Il design deve anticipare questi casi fin dall'inizio.

Pronto a superare i tuoi colloqui su iOS?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

7. Come rendere accessibili le immagini?

Le immagini richiedono trattamenti diversi in base al loro ruolo: quelle informative necessitano di descrizione, quelle decorative vanno ignorate, quelle interattive richiedono label di azione.

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 { }
    }
}

Un'immagine descritta male o un'immagine decorativa non mascherata degrada notevolmente l'esperienza con VoiceOver.

Raggruppamento e navigazione avanzata

8. Come si raggruppano gli elementi per VoiceOver?

Il raggruppamento combina più elementi visivi in un unico elemento accessibile. Questo accelera la navigazione e fornisce il contesto completo in un singolo annuncio.

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 { }
    }
}

Senza raggruppamento, VoiceOver si ferma su ciascun Text individualmente, rallentando notevolmente la navigazione.

9. Come implementare azioni di accessibilità personalizzate?

Le azioni personalizzate aggiungono funzionalità accessibili senza appesantire l'interfaccia visiva. Gli utenti VoiceOver vi accedono tramite swipe verticale.

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
    }
}

Le azioni personalizzate sostituiscono vantaggiosamente i gesti di swipe, difficili da scoprire con VoiceOver.

10. Come gestire il focus di VoiceOver in modo programmatico?

Controllare il focus è essenziale dopo cambiamenti dell'interfaccia: comparsa di una modale, caricamento di contenuto, navigazione. Il focus deve guidare l'utente verso l'informazione rilevante.

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"
        )
    }
}

Le notifiche .screenChanged e .layoutChanged informano VoiceOver dei cambiamenti significativi nell'interfaccia.

Pronto a superare i tuoi colloqui su iOS?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Elementi regolabili e controlli complessi

11. Come rendere accessibile uno slider personalizzato?

Uno slider personalizzato deve implementare il trait .adjustable e rispondere ai gesti di incremento/decremento. VoiceOver utilizza lo swipe verticale per modificare il valore.

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)
    }
}

Senza il trait .adjustable, uno slider personalizzato richiederebbe di toccare ogni stella singolarmente.

12. Come creare un carosello accessibile?

I caroselli pongono sfide di accessibilità: navigazione non lineare, contenuto fuori schermo e stati multipli. La soluzione combina il trait .adjustable con annunci contestuali.

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)
    }
}

Questo approccio permette di navigare il carosello senza dipendere dai gesti di swipe standard.

13. Come gestire l'accessibilità dei form?

I form accessibili associano ogni campo alla sua label, indicano gli errori con chiarezza e consentono una navigazione fluida tra i campi.

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"
    }
}

Gli hint di accessibilità comunicano errori e istruzioni senza appesantire l'interfaccia visiva.

Audit e test di accessibilità

14. Come si fa un audit dell'accessibilità di un'app iOS?

L'audit combina strumenti automatizzati e test manuali. L'Accessibility Inspector di Xcode analizza gli elementi, VoiceOver convalida l'esperienza reale e i test unitari verificano le proprietà di accessibilità.

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()
    }
}

L'audit automatico di iOS 17+ rileva problemi comuni: label mancanti, contrasto insufficiente, elementi troppo piccoli.

15. Quali sono gli errori di accessibilità più comuni?

Gli errori frequenti includono label mancanti, immagini non descritte, mancanza di supporto a Dynamic Type ed elementi interattivi senza trait appropriati.

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)

Questi errori sono rilevabili con l'Accessibility Inspector e correggibili con poche righe di codice.

Conclusione

L'accessibilità iOS poggia su tre pilastri: VoiceOver per gli utenti con disabilità visive, Dynamic Type per l'adattamento del testo e i trait semantici per una navigazione coerente. Padroneggiare questi concetti dimostra competenze professionali molto richieste dalle aziende.

Checklist di revisione

  • Configurare VoiceOver con label, hint e trait appropriati
  • Implementare Dynamic Type con stili di testo semantici
  • Adattare i layout per dimensioni di accessibilità estreme
  • Raggruppare gli elementi per una navigazione efficiente
  • Gestire il focus in modo programmatico dopo cambiamenti dell'interfaccia
  • Rendere regolabili i controlli personalizzati
  • Effettuare audit regolari con l'Accessibility Inspector e test automatizzati
  • Evitare errori comuni: label mancanti, immagini non descritte, contrasto insufficiente

Risorse aggiuntive

La documentazione ufficiale di Apple "Human Interface Guidelines - Accessibility" rimane il riferimento. Testare regolarmente con VoiceOver attivo aiuta a individuare problemi che gli strumenti automatici non rilevano.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

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

Condividi

Articoli correlati