Câu hỏi phỏng vấn về khả năng tiếp cận iOS năm 2026: VoiceOver và Dynamic Type

Chuẩn bị phỏng vấn iOS với những câu hỏi then chốt về khả năng tiếp cận: VoiceOver, Dynamic Type, các trait ngữ nghĩa và kiểm thử.

Câu hỏi phỏng vấn khả năng tiếp cận iOS: VoiceOver và Dynamic Type

Khả năng tiếp cận đã trở thành kỹ năng then chốt trong phát triển iOS. Nhà tuyển dụng hiện đánh giá có hệ thống mức độ thành thạo VoiceOver, Dynamic Type và các API trợ năng. Những câu hỏi sau bao quát các khái niệm cốt lõi để thành công trong phỏng vấn kỹ thuật.

Vì sao khả năng tiếp cận quan trọng trong phỏng vấn

Hơn 1 tỷ người sống chung với khuyết tật. Apple áp đặt tiêu chuẩn trợ năng nghiêm ngặt và nhiều công ty từ chối phát hành ứng dụng không khả dụng. Năng lực này tạo ra khác biệt giữa ứng viên senior và phần còn lại.

Nền tảng khả năng tiếp cận trên iOS

1. Các công cụ trợ năng chính của iOS là gì?

iOS cung cấp một hệ sinh thái công cụ trợ năng đầy đủ phục vụ nhiều dạng khuyết tật khác nhau. VoiceOver hỗ trợ đọc màn hình cho người khiếm thị. Dynamic Type điều chỉnh kích thước văn bản. Switch Control cho phép điều khiển bằng các công tắc bên ngoài. Voice Control cung cấp điều khiển bằng giọng nói toàn diện.

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

Các API này cho phép điều chỉnh giao diện một cách linh hoạt theo tùy chọn của người dùng và công nghệ trợ giúp đang hoạt động.

2. VoiceOver hoạt động kỹ thuật như thế nào?

VoiceOver duyệt qua giao diện và xác định các thành phần khả dụng. Mỗi thành phần phơi bày các thuộc tính như label, value, traits và actions. Trình đọc màn hình tổng hợp những thông tin đó bằng giọng nói khi người dùng điều hướng bằng cử chỉ.

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

Hệ thống thứ bậc trợ năng có thể khác với hệ thống thứ bậc trực quan, cho phép điều hướng hợp lý ngay cả khi bố cục phức tạp.

3. Sự khác biệt giữa accessibilityLabel và accessibilityValue là gì?

accessibilityLabel định danh thành phần một cách bền vững, còn accessibilityValue thể hiện trạng thái hiện tại có thể thay đổi. Sự phân biệt này rất quan trọng với các điều khiển động như slider hay 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 luôn đọc label trước rồi đến value, giúp người dùng hiểu thành phần là gì và đang ở trạng thái nào.

4. Giải thích accessibilityTraits và tầm quan trọng

Traits cung cấp cho VoiceOver thông tin về kiểu và hành vi của một thành phần. Chúng quyết định cách thành phần được công bố và những hành động khả dụng. Một nút thiếu trait .button sẽ không được công bố là nút.

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 giúp công nghệ trợ giúp mang lại trải nghiệm nhất quán và có thể đoán trước cho người dùng.

Dynamic Type và kích thước văn bản thích ứng

5. Cách triển khai Dynamic Type đúng cách?

Dynamic Type cho phép người dùng điều chỉnh kích thước văn bản hệ thống. Triển khai đúng sử dụng các kiểu chữ ngữ nghĩa và ràng buộc linh hoạt để giao diện tự thích ứng.

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

Không có adjustsFontForContentSizeCategory, các label vẫn giữ kích thước ban đầu dù người dùng có thay đổi tùy chọn.

6. Cách xử lý các kích thước trợ năng cực đại?

Các kích thước trợ năng (AX1 đến AX5) có thể tăng kích thước văn bản gấp ba. Giao diện phải thích ứng bằng các bố cục thay thế: xếp dọc thay vì ngang, ẩn các thành phần trang trí và điều chỉnh khoảng cách.

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

Các hạng mục trợ năng bắt đầu từ .accessibilityMedium (AX1). Thiết kế cần lường trước các trường hợp này ngay từ đầu.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

7. Cách làm cho hình ảnh dễ tiếp cận?

Hình ảnh cần xử lý khác nhau theo vai trò: hình ảnh thông tin cần mô tả, hình ảnh trang trí cần được bỏ qua, hình ảnh tương tác cần nhãn hành động.

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

Một hình ảnh được mô tả kém hoặc một hình ảnh trang trí không được ẩn sẽ làm xấu trải nghiệm VoiceOver đáng kể.

Nhóm và điều hướng nâng cao

8. Cách nhóm các thành phần cho VoiceOver?

Việc nhóm gộp nhiều thành phần trực quan thành một thành phần khả dụng duy nhất. Điều này tăng tốc điều hướng và cung cấp ngữ cảnh đầy đủ trong một thông báo.

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

Không có việc nhóm, VoiceOver dừng tại từng Text riêng lẻ, làm chậm điều hướng đáng kể.

9. Cách triển khai hành động trợ năng tùy chỉnh?

Các hành động tùy chỉnh thêm chức năng có thể tiếp cận mà không làm rối giao diện trực quan. Người dùng VoiceOver truy cập chúng bằng cách vuốt theo chiều dọc.

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

Hành động tùy chỉnh thay thế tốt cho các cử chỉ vuốt khó khám phá khi dùng VoiceOver.

10. Cách quản lý tiêu điểm VoiceOver theo cách lập trình?

Kiểm soát tiêu điểm là tối quan trọng sau các thay đổi giao diện: hộp thoại xuất hiện, tải nội dung, điều hướng. Tiêu điểm phải dẫn người dùng đến thông tin liên quan.

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

Các thông báo .screenChanged.layoutChanged thông báo cho VoiceOver về những thay đổi đáng kể trên giao diện.

Sẵn sàng chinh phục phỏng vấn iOS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Thành phần điều chỉnh được và điều khiển phức tạp

11. Cách làm cho slider tùy chỉnh có thể tiếp cận?

Một slider tùy chỉnh phải triển khai trait .adjustable và đáp ứng các cử chỉ tăng/giảm. VoiceOver dùng vuốt dọc để thay đổi giá trị.

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

Không có trait .adjustable, slider tùy chỉnh sẽ buộc người dùng chạm vào từng ngôi sao một.

Carousel đặt ra thách thức trợ năng: điều hướng phi tuyến, nội dung ngoài màn hình và nhiều trạng thái. Giải pháp kết hợp trait .adjustable với các thông báo theo ngữ cảnh.

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

Cách tiếp cận này cho phép điều hướng carousel mà không phụ thuộc vào cử chỉ vuốt tiêu chuẩn.

13. Cách xử lý khả năng tiếp cận của biểu mẫu?

Các biểu mẫu có thể tiếp cận liên kết mỗi trường với label của nó, chỉ ra lỗi rõ ràng và cho phép điều hướng mượt mà giữa các trường.

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

Hints trợ năng truyền tải lỗi và hướng dẫn mà không làm rối giao diện trực quan.

Kiểm thử và đánh giá khả năng tiếp cận

14. Cách đánh giá khả năng tiếp cận của một ứng dụng iOS?

Đánh giá kết hợp công cụ tự động và thử nghiệm thủ công. Accessibility Inspector của Xcode phân tích các thành phần, VoiceOver xác nhận trải nghiệm thực và kiểm thử đơn vị xác minh các thuộc tính trợ năng.

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

Kiểm tra tự động trong iOS 17+ phát hiện các vấn đề phổ biến: thiếu label, độ tương phản kém, các thành phần quá nhỏ.

15. Những lỗi trợ năng phổ biến nhất là gì?

Những lỗi thường gặp gồm thiếu label, hình ảnh không có mô tả, thiếu hỗ trợ Dynamic Type và các thành phần tương tác không có traits phù hợp.

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)

Những lỗi này có thể phát hiện bằng Accessibility Inspector và sửa với vài dòng mã.

Kết luận

Khả năng tiếp cận trên iOS dựa trên ba trụ cột: VoiceOver cho người khiếm thị, Dynamic Type để thích ứng văn bản và traits ngữ nghĩa cho điều hướng nhất quán. Thành thạo các khái niệm này thể hiện chuyên môn được nhà tuyển dụng tích cực tìm kiếm.

Danh sách kiểm tra

  • Cấu hình VoiceOver với label, hint và traits phù hợp
  • Triển khai Dynamic Type với kiểu chữ ngữ nghĩa
  • Điều chỉnh bố cục cho các kích thước trợ năng cực đại
  • Nhóm các thành phần để điều hướng hiệu quả
  • Quản lý tiêu điểm bằng lập trình sau các thay đổi giao diện
  • Làm cho các điều khiển tùy chỉnh có thể điều chỉnh
  • Đánh giá thường xuyên với Accessibility Inspector và kiểm thử tự động
  • Tránh lỗi phổ biến: thiếu label, hình ảnh không mô tả, độ tương phản kém

Tài nguyên bổ sung

Tài liệu chính thức của Apple "Human Interface Guidelines - Accessibility" vẫn là tài liệu tham khảo. Thử nghiệm thường xuyên với VoiceOver bật giúp phát hiện các vấn đề mà công cụ tự động không thấy.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

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

Chia sẻ

Bài viết liên quan