Irbisa · Шпаргалка
Доступность и локализация
VoiceOver, traits, Dynamic Type, Reduce Motion, String Catalog, плюрализация, RTL, форматы локали
Что входит в доступность iOS-приложения: для каких пользователей вы его делаете и какие системные функции поддерживаете?
Что такое accessibility label, value, traits и hint у элемента и в каком порядке VoiceOver их читает?
// wrong: type and state are baked into the labelButton(action: toggle) { Image(systemName: icon) } .accessibilityLabel(isOn ? "Favourite button, on" : "Favourite button, off") // right: each piece in its own propertyButton(action: toggle) { Image(systemName: icon) }…Что произносит VoiceOver, проходя по этой строке свайпами, и что бы вы изменили?
struct UserRow: View { let user: User let onDelete: () -> Void var body: some View { HStack { Image("avatar_placeholder") Text(user.name) Spacer() Button(action: onDelete) { Image("ic_trash") } } }}Что такое Dynamic Type и как поддержать его в SwiftUI и UIKit?
// SwiftUIText("Order history").font(.headline) // scalesText(order.note) .font(.custom("Brand", size: 15, relativeTo: .subheadline))Text("v2.4.1").font(.system(size: 11)) // fixed: never scales…Как локализовать iOS-приложение и что нельзя хардкодить?
// SwiftUI: literals are localization keysText("Order history")Button("Delete", role: .destructive) { delete() } // Outside a view: resolve now, and give the translator contextlet title = String(localized: "Delete order?",…Карточка товара состоит из пяти остановок VoiceOver: картинка, название, цена, бейдж, кнопка-сердечко. Как сделать одну, и когда этого делать не стоит?
HStack { AsyncImage(url: product.imageURL) .accessibilityHidden(true) // decoration: hide before combining VStack(alignment: .leading) { Text(product.name) Text(product.price.formatted(.currency(code: product.currency)))…VoiceOver читает цену карточки раньше названия, хотя название нарисовано выше. Как управлять порядком чтения?
// SwiftUI: price is declared first but drawn below the title; the title must be read firstZStack(alignment: .topLeading) { Text(album.price) .offset(y: 24) .accessibilitySortPriority(1) Text(album.title)…В строке есть swipe-to-delete и кастомный регулятор громкости. Как пользователь VoiceOver доберётся до них без этих жестов?
MessageRow(message: message) .accessibilityAction(named: "Delete") { delete(message) } .accessibilityAction(named: "Mark as read") { markRead(message) } VolumeDial(value: $volume) .accessibilityLabel("Volume")…После сохранения VoiceOver прыгает в начало экрана и читает его заново. Какое уведомление отправили по ошибке и какое нужно было?
// wrong: resets the user's position and re-reads the screenUIAccessibility.post(notification: .screenChanged, argument: "Saved") // right: speak, and leave focus aloneUIAccessibility.post(notification: .announcement, argument: String(localized: "Draft saved"))…Форма не прошла валидацию. Как в SwiftUI перевести фокус VoiceOver на сообщение об ошибке?
struct SignUpForm: View { @State private var email = "" @State private var error: String? @AccessibilityFocusState private var errorFocused: Bool var body: some View {…QA сообщает, что эта строка игнорирует размер текста пользователя, а когда начинает масштабироваться, обрезается на самых крупных размерах. Что вы измените?
HStack(spacing: 12) { Image(systemName: "envelope") .frame(width: 40, height: 40) Text(item.title) .font(.system(size: 17)) .lineLimit(1)}.frame(height: 44)Найдите баг: пользователь увеличивает размер текста через Пункт управления, и подписи в этой шапке растут, но остаются в ряд и обрезаются, пока экран не открыть заново.
final class ContactHeaderViewController: UIViewController { private let stack = UIStackView(arrangedSubviews: [UILabel(), UILabel()]) override func viewDidLoad() { super.viewDidLoad() for case let label as UILabel in stack.arrangedSubviews { label.font = .preferredFont(forTextStyle: .body) label.adjustsFontForContentSizeCategory = true } view.addSubview(stack) stack.frame = view.bounds } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) stack.axis = traitCollection.preferredContentSizeCategory.isAccessibilityCategory ? .vertical : .horizontal }}Что такое Reduce Motion, Reduce Transparency, Increase Contrast и Bold Text и как учитывать их в своём UI?
struct Hero: View { let isOpen: Bool let parallaxOffset: CGFloat @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.accessibilityReduceTransparency) private var reduceTransparency @Environment(\.colorSchemeContrast) private var contrast…Дизайн помечает оплаченные заказы зелёной точкой, а просроченные красной. Что с этим не так и как это исправить?
struct OrderRow: View { let order: Order var body: some View { HStack { Image(systemName: order.isPaid ? "checkmark.circle.fill"…Как тестировать доступность приложения: что находят Accessibility Inspector и
performAccessibilityAudit(), а что они пропускают?final class AccessibilityAuditTests: XCTestCase { func testHomeScreenPassesAudit() throws { let app = XCUIApplication() app.launch() try app.performAccessibilityAudit(for: [.contrast, .hitRegion, .textClipped]) { issue in…Что такое String Catalog и что значат состояния New, Needs Review и Stale после сборки?
Строка задана как
"\(count) items", а в русской сборке выходит «1 товаров». Как правильно работать с множественным числом?// wrong: an English-only rule baked into the call sitelet label = count == 1 ? "1 item" : "\(count) items" // right: one key, every language brings its own formsText("\(count) items") // catalog key: "%lld items"…В немецкой сборке заголовок навигации переведён, а заголовки секций остались английскими. Почему и какой тип это исправляет?
struct LibrarySection: Identifiable { let id = UUID() let title: String} let sections = [LibrarySection(title: "Recent"), LibrarySection(title: "Favourites")] List(sections) { section in Text(section.title)}.navigationTitle("Library")Как форматировать даты, числа и цены для пользователя и кто выбирает формат, если язык английский, а регион немецкий?
// wrong: the developer chose the pattern and the symbollet df = DateFormatter()df.dateFormat = "MM/dd/yyyy hh:mm a"Text("$\(order.total)") // right: the locale decides presentation, the data decides the currency…Найдите баг: во французской и немецкой сборках пикера стран несколько стран оказываются в самом конце списка.
struct CountryPicker: View { let regionCodes: [String] // "AE", "AT", "DE", "ZW", … @Binding var selection: String private var countries: [(code: String, name: String)] { regionCodes .map { ($0, Locale.current.localizedString(forRegionCode: $0) ?? $0) } .sorted { $0.name < $1.name } } var body: some View { Picker("Country", selection: $selection) { ForEach(countries, id: \.code) { Text(verbatim: $0.name).tag($0.code) } } }}Продукт хочет переключатель языка внутри приложения: что уже даёт iOS и почему запись
AppleLanguagesвUserDefaultsсчитается хаком?Как поддержать языки с письмом справа налево, например арабский: что зеркалится автоматически, что переворачивать самому и что нельзя зеркалить никогда?
// A directional custom icon: mirror it in RTLImage("arrow_next") .flipsForRightToLeftLayoutDirection(true) // Media controls keep their direction in a mirrored screenPlayerControls()…Столбчатый график нарисован в одном
Canvasили в переопределённомdraw(_:). Как сделать каждый столбец доступным для VoiceOver?struct SalesChart: View { let sales: [MonthlySales] // Identifiable: month, amount var body: some View { Canvas { context, size in drawBars(sales, in: context, size: size) } .accessibilityLabel("Sales by month") .accessibilityChildren { // never drawn, only exposed to VoiceOver…Переводы приходят за две недели до релиза. Как поймать обрезанный текст и захардкоженные строки в CI задолго до этого?
final class LocalizationLayoutTests: XCTestCase { func testCheckoutFitsLongStringsAndLargeText() throws { let app = XCUIApplication() // Language and region come from the test plan configuration app.launchArguments += [ "-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXXL",…Помимо UI приложения, где локализуются страница в App Store, push-уведомления и App Shortcuts?