Dünya nüfusunun %15'i bir engele sahip. iOS'un erişilebilirlik özellikleri, uygulamanı tüm kullanıcılar için kullanılabilir kılar. Apple, erişilebilirlik konusunda endüstrinin lideri — ve App Store'da erişilebilirlik standartlarını karşılamayan uygulamalar reddedilebilir.
💡 Hızlı Not: Bu rehber Apple'ın Human Interface Guidelines Accessibility bölümü ve WCAG 2.1 AA standartlarından derlendi.
İçindekiler
- Neden Erişilebilirlik?
- VoiceOver Temelleri
- SwiftUI Accessibility Modifiers
- Dynamic Type
- Renk Kontrastı ve Color Blindness
- Reduce Motion
- Custom Accessibility Actions
- Accessibility Audit (Xcode)
- WCAG Compliance
- Testing Checklist
Neden Erişilebilirlik? {#neden-a11y}
İstatistik | Değer |
|---|---|
Dünya engelli nüfusu | 1.3 milyar (%15) |
iOS VoiceOver kullanıcısı | Milyonlarca |
Renk körlüğü oranı | Erkeklerin %8'i |
Yaşla gelen görme sorunu | 50+ yaş %50+ |
App Store erişilebilirlik reddi | Artan trend |
VoiceOver Temelleri {#voiceover}
VoiceOver, iOS'un ekran okuyucusudur. Görme engelli kullanıcılar uygulamanı parmak hareketleriyle yönetir ve VoiceOver ekrandakileri sesli okur.
swift
1// Temel accessibility label2Image(systemName: "heart.fill")3 .accessibilityLabel("Favori")4 5// Daha detaylı6Button(action: addToCart) {7 Image(systemName: "cart.badge.plus")8}9.accessibilityLabel("Sepete ekle")10.accessibilityHint("Ürünü alışveriş sepetinize ekler")11.accessibilityAddTraits(.isButton)SwiftUI Accessibility Modifiers {#swiftui-a11y}
swift
1// Accessibility modifier'lar2struct ProductCard: View {3 let product: Product4 5 var body: some View {6 VStack {7 AsyncImage(url: product.imageURL)8 .accessibilityHidden(true) // Dekoratif image9 10 Text(product.name)11 .font(.headline)12 13 Text("\(product.price, format: .currency(code: "TRY"))")14 .font(.subheadline)15 16 HStack {17 ForEach(0..<5) { index in18 Image(systemName: index < product.rating ? "star.fill" : "star")19 }20 }21 .accessibilityElement(children: .ignore)22 .accessibilityLabel("\(product.rating) üzerinden 5 yıldız")23 }24 // Tüm card tek element olarak okunur25 .accessibilityElement(children: .combine)26 .accessibilityLabel("\(product.name), \(product.price) TL, \(product.rating) yıldız")27 .accessibilityAddTraits(.isButton)28 }29}30 31// Rotor actions - VoiceOver kullanıcıları için hızlı eylemler32Text(article.title)33 .accessibilityAction(named: "Paylaş") { shareArticle(article) }34 .accessibilityAction(named: "Yer İmi") { bookmarkArticle(article) }35 36// Custom accessibility value37Slider(value: $volume, in: 0...100)38 .accessibilityValue("Ses seviyesi yüzde \(Int(volume))")Dynamic Type {#dynamic-type}
swift
1// SwiftUI - otomatik Dynamic Type desteği2Text("Başlık")3 .font(.title) // Otomatik ölçeklenir4 5// Custom font ile Dynamic Type6Text("Custom")7 .font(.custom("Avenir", size: 16, relativeTo: .body))8 9// Minimum/maksimum boyut10Text("Sınırlı")11 .dynamicTypeSize(.small ... .xxxLarge) // Aralık belirle12 13// Layout adaptasyonu14@Environment(\.dynamicTypeSize) var dynamicTypeSize15 16var body: some View {17 if dynamicTypeSize >= .accessibility1 {18 // Büyük text için dikey layout19 VStack { content }20 } else {21 // Normal text için yatay layout22 HStack { content }23 }24}Renk Kontrastı ve Color Blindness {#renk-kontrast}
swift
1// Minimum kontrast oranı: 4.5:1 (normal text), 3:1 (büyük text)2 3// ✅ İyi kontrast4Text("Okunabilir")5 .foregroundStyle(.primary) // Otomatik kontrast6 7// Renk körlüğü desteği8// Sadece renge bağımlı olma - ikon/şekil de kullan9HStack {10 Image(systemName: "checkmark.circle.fill")11 .foregroundStyle(.green)12 Text("Başarılı")13}14// ❌ Sadece renk ile durum belirtme15Circle().fill(.red) // Renk körü kullanıcı anlayamaz16 17// differentiateWithoutColor18@Environment(\.accessibilityDifferentiateWithoutColor) var differentiateWithoutColor19 20var statusView: some View {21 if differentiateWithoutColor {22 Label("Hata", systemImage: "xmark.circle")23 } else {24 Circle().fill(.red)25 }26}Reduce Motion {#reduce-motion}
swift
1@Environment(\.accessibilityReduceMotion) var reduceMotion2 3var body: some View {4 content5 .animation(reduceMotion ? nil : .spring(), value: isExpanded)6 .transition(reduceMotion ? .opacity : .slide)7}8 9// Otomatik hareket kontrolü10withAnimation(reduceMotion ? nil : .easeInOut(duration: 0.3)) {11 isVisible.toggle()12}Custom Accessibility Actions {#custom-actions}
swift
1// Swipe actions alternatifi - VoiceOver kullanıcıları için2struct MessageRow: View {3 var body: some View {4 Text(message.text)5 .accessibilityAction(named: "Yanıtla") { reply() }6 .accessibilityAction(named: "Sil") { delete() }7 .accessibilityAction(named: "İlet") { forward() }8 }9}10 11// Custom adjustable - artır/azalt12struct QuantityPicker: View {13 @Binding var quantity: Int14 15 var body: some View {16 HStack {17 Button("-") { quantity -= 1 }18 Text("\(quantity)")19 Button("+") { quantity += 1 }20 }21 .accessibilityElement(children: .ignore)22 .accessibilityLabel("Adet")23 .accessibilityValue("\(quantity)")24 .accessibilityAdjustableAction { direction in25 switch direction {26 case .increment: quantity += 127 case .decrement: quantity = max(0, quantity - 1)28 @unknown default: break29 }30 }31 }32}Accessibility Audit (Xcode) {#audit}
- Xcode Accessibility Inspector: Developer Tools'dan aç
- Audit button: Mevcut ekranın erişilebilirlik sorunlarını tara
- Inspection: Her element'in label, value, traits bilgisi
- Simulator: Settings > Accessibility > VoiceOver ile test
WCAG Compliance {#wcag}
Level | Gereksinim | iOS Karşılığı |
|---|---|---|
A | Temel erişilebilirlik | accessibilityLabel, traits |
AA | Yeterli erişilebilirlik | 4.5:1 kontrast, Dynamic Type |
AAA | İleri erişilebilirlik | Reduce motion, full VoiceOver |
Testing Checklist {#testing}
- ✅ VoiceOver ile tüm ekranları test et
- ✅ Dynamic Type XXL ile layout kontrol et
- ✅ Bold Text açıkken kontrol et
- ✅ Renk kontrastı 4.5:1 minimum
- ✅ Touch target 44x44pt minimum
- ✅ Reduce Motion açıkken animasyonlar
- ✅ Keyboard navigation (iPad)
- ✅ Switch Control uyumluluğu
Accessibility Notifications {#a11y-notifications}
VoiceOver kullanıcılarına dinamik değişiklikleri bildirmek için accessibility notification'lar kullan:
swift
1// Ekran değişikliği bildirimi - yeni ekran yüklendiğinde2UIAccessibility.post(notification: .screenChanged, argument: headerView)3 4// Layout değişikliği - ekranın bir bölümü güncellendiğinde5UIAccessibility.post(notification: .layoutChanged, argument: updatedLabel)6 7// Announcement - önemli bilgi duyurusu8UIAccessibility.post(notification: .announcement, argument: "Sipariş başarıyla oluşturuldu")9 10// SwiftUI'da announcement11struct OrderView: View {12 @State private var orderStatus = ""13 14 var body: some View {15 VStack {16 Text(orderStatus)17 .accessibilityLabel("Sipariş durumu: \(orderStatus)")18 }19 .onChange(of: orderStatus) { _, newValue in20 // VoiceOver kullanıcısına otomatik duyuru21 AccessibilityNotification.Announcement(newValue).post()22 }23 }24}25 26// Priority announcement (iOS 17+) - diğer duyuruları kesebilir27AccessibilityNotification.Announcement("Acil uyarı: Oturum süresi dolmak üzere")28 .post()Notification Tipleri
Notification | Ne Zaman Kullanılır | VoiceOver Davranışı |
|---|---|---|
.screenChanged | Yeni ekran/modal açıldığında | Focus ilk elemente atlar |
.layoutChanged | Kısmi UI güncellemesinde | Focus belirtilen elemente gider |
.announcement | Durum bildirimi | Mevcut okumayı kesmeden duyurur |
.pageScrolled | Sayfa scroll edildiğinde | "Sayfa 2/5" gibi bilgi verir |
Switch Control ve Keyboard Navigation {#switch-control}
Switch Control, motor engelli kullanıcıların tek veya birkaç butonla cihazı kullanmasını sağlar. Keyboard navigation ise iPad ve Mac Catalyst uygulamalarında kritik:
swift
1// Focus sistemi - keyboard ve switch control için2struct NavigableList: View {3 @FocusState private var focusedItem: String?4 let items: [MenuItem]5 6 var body: some View {7 VStack {8 ForEach(items) { item in9 Button(action: { select(item) }) {10 MenuItemRow(item: item)11 }12 .focused($focusedItem, equals: item.id)13 .focusEffectDisabled(false) // Focus ring göster14 .accessibilityFocused($focusedItem, equals: item.id)15 }16 }17 .focusSection() // Tab ile bu gruba gelinebilir18 .onKeyPress(.return) {19 if let focused = focusedItem { activate(focused) }20 return .handled21 }22 .onKeyPress(.escape) {23 focusedItem = nil24 return .handled25 }26 }27}28 29// Accessibility focus management30struct DetailView: View {31 @AccessibilityFocusState var isHeaderFocused: Bool32 33 var body: some View {34 VStack {35 Text("Detay Başlığı")36 .font(.title)37 .accessibilityFocused($isHeaderFocused)38 39 // ... içerik40 }41 .onAppear {42 // Ekran açıldığında VoiceOver focus'unu başlığa taşı43 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {44 isHeaderFocused = true45 }46 }47 }48}Accessibility Containers ve Grouping {#containers}
Karmaşık UI'larda element'leri mantıklı gruplara ayırmak VoiceOver deneyimini dramatik şekilde iyileştirir:
swift
1// Karmaşık card'ı tek anlamlı element yap2struct TransactionCard: View {3 let transaction: Transaction4 5 var body: some View {6 HStack {7 VStack(alignment: .leading) {8 Text(transaction.merchant)9 .font(.headline)10 Text(transaction.date.formatted(date: .abbreviated, time: .shortened))11 .font(.caption)12 .foregroundStyle(.secondary)13 }14 Spacer()15 Text(transaction.amount.formatted(.currency(code: "TRY")))16 .font(.title3)17 .foregroundStyle(transaction.amount < 0 ? .red : .green)18 }19 .padding()20 // Tüm child'ları birleştir - VoiceOver tek element olarak okur21 .accessibilityElement(children: .combine)22 // Özel okuma sırası ve formatı23 .accessibilityLabel(24 "\(transaction.merchant), " +25 "\(transaction.amount < 0 ? "harcama" : "gelir") " +26 "\(abs(transaction.amount).formatted(.currency(code: "TRY"))), " +27 "\(transaction.date.formatted(date: .abbreviated, time: .shortened))"28 )29 // Swipe action'lar30 .accessibilityAction(named: "Detay Görüntüle") { showDetail(transaction) }31 .accessibilityAction(named: "Tekrarla") { repeatTransaction(transaction) }32 }33}34 35// Accessibility scroll - büyük listeler için36struct InfiniteScrollList: View {37 @State private var page = 138 39 var body: some View {40 ScrollView {41 LazyVStack {42 ForEach(items) { item in43 ItemRow(item: item)44 }45 }46 }47 .accessibilityScrollAction { edge in48 if edge == .bottom {49 page += 150 loadMore()51 // Scroll sonucunu VoiceOver'a bildir52 UIAccessibility.post(53 notification: .pageScrolled,54 argument: "Sayfa \(page) yüklendi, \(items.count) sonuç"55 )56 }57 }58 }59}Erişilebilirlik Environment Değerleri
Environment Key | Tip | Açıklama |
|---|---|---|
\.accessibilityReduceMotion | Bool | Hareket azaltma |
\.accessibilityReduceTransparency | Bool | Şeffaflık azaltma |
\.accessibilityDifferentiateWithoutColor | Bool | Renksiz ayırt etme |
\.accessibilityShowButtonShapes | Bool | Buton şekillerini göster |
\.accessibilityInvertColors | Bool | Renk tersine çevirme |
\.accessibilityVoiceOverEnabled | Bool | VoiceOver aktif mi |
\.dynamicTypeSize | DynamicTypeSize | Metin boyutu tercihi |
\.legibilityWeight | LegibilityWeight | Bold Text tercihi |
Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var: **Kaynaklar:** - [Apple: Accessibility](https://developer.apple.com/accessibility/) - [WWDC23: Build accessible apps](https://developer.apple.com/videos/play/wwdc2023/10034/) - [WCAG 2.1 Guidelines](https://www.w3.org/TR/WCAG21/)

