Вот и закончилась очередная конференция Apple WWDC23, на которой ребята из Apple рассказали об изменениях Xcode и Swift.
Я — Никита Коробейников, iOS Team Lead в Surf. Уже поставил бета-версию Xcode 15 и проверил на автотестах материал с сессий Fix failures faster with Xcode test reports и Perform accessibility audits for your app. Рассказываю, почему разработчикам гораздо удобнее теперь делать приложение доступным.
Небольшое отступление
В нашей студийной библиотеке RDDM есть Example-проект: в нем демонстрируются различные кейсы для наполнения и расширения функциональности UITableView
и UICollectionView
. Проект немаленький: в нем около сорока экранов и есть автотесты.
Мы активно используем RDDM в продакшене, поэтому ещё давно написали автотесты, контролирующие работу библиотеки. А в roadmap библиотеки была задача о поддержке доступности (accessibility) в Example-проекте.
Мы не знали тем выступлений WWDC заранее, но изменения в отчётах о тестировании и авто-аудит доступности (accessibility) были как будто спроектированы специально для наших задач.
Далее в статье будем использовать тест-кейс, который:
Запускает приложение.
Выбирает один из трёх табов: Collection, Table, Stack.
Перебирает каждый элемент списка и:
кликает по элементу,
проводит аудит,
кликает на кнопку Back.
import XCTest
class ReactiveDataDisplayManagerExampleUITests: XCTestCase {
override func setUpWithError() throws {
try super.setUpWithError()
continueAfterFailure = true
app = XCUIApplication()
app.launchArguments.append("-disableAnimations")
app.launchArguments.append(contentsOf: additionalCommands)
app.launch()
}
func testCollectionScreen() throws {
app.tabBars.buttons["Collection"].tap()
try assertAllScreensOpeningWithoutCrashes()
}
func testTableScreen() throws {
app.tabBars.buttons["Table"].tap()
try assertAllScreensOpeningWithoutCrashes()
}
func testStackScreen() throws {
app.tabBars.buttons["Stack"].tap()
try assertAllScreensOpeningWithoutCrashes()
}
}
// MARK: - Private methods
private extension ReactiveDataDisplayManagerExampleUITests {
func assertAllScreensOpeningWithoutCrashes() throws {
let tablesQuery = app.tables
for i in 0...tablesQuery.cells.count - 1 {
tablesQuery.cells.element(boundBy: i).tap()
try app.performAccessibilityAudit()
app.navigationBars.firstMatch.buttons["Back"].tap()
}
}
}
Таким образом мы автоматически проверим, насколько Example-проект доступен для людей с ограниченными возможностями, и попробуем разрешить некоторые проблемы.
Обзор отчётов о тестировании
Запустим тест. Флаг continueAfterFailure
не будет прерывать прогон тестов при провале аудита или других ошибках, поэтому подождём немного, скрестив пальцы.
Тесты упали, зато мы можем посмотреть новый отчёт о тестировании.
UI главного отчёта понятный — как код опытного сеньора-помидора. Самое интересное — в инсайтах (Insights). Провалимся в разбор теста testTableScreen
.
Apple добавил в Xcode 15 полную запись прогона теста.
Слева — хронологический список действий:
поиск объекта,
событие нажатия,
проверки присутствия элементов,
ошибки.
Снизу — интерактивный таймлайн. Справа — текущий кадр
На таймлайне можно быстро перемещаться прямо к месту падения теста. Очень удобно, когда тест упал много раз, как в нашем случае.
События, связанные с нажатием, подсвечиваются на текущем кадре.
Не хватает только возможности поделиться записанным видео, чтобы его прикрепить к багу в Jira или другом issue-tracker. Во вкладке Gallery
можно скачать артефакт, но он будет содержать лишь последний кадр (баг бета-версии Xcode? Может быть).
Аудит доступности
Apple большое внимание уделяет доступности своих устройств для людей с ограничениями зрения, слуха и так далее. Инженеры проектируют альтернативные способы управления устройствами Apple или специальные возможности:
увеличение текста,
VoiceOver — чтение содержимого экрана,
оптимизация световых вспышек на видео,
остановка воспроизведения анимированных GIF-изображений.
Тестирование доступности — задача не из легких. Существуют даже компании со специально обученными QA, которые занимаются таким тестированием.
Чтобы автоматизировать процесс тестирования доступности, Apple добавил новый метод.
XCUIApplication.performAccessibilityAudit(
for auditTypes: XCUIAccessibilityAuditType = .all,
_ issueHandler: ((XCUIAccessibilityAuditIssue) throws -> Bool)? = nil)
auditTypes
позволяет выбрать набор сценариев аудита
issueHandler
позволяет игнорировать ошибки конкретных элементов, данные которых хранятся в XCUIAccessibilityAuditIssue
Чтобы понять, как работает метод performAccessibilityAudit
, посмотрим отрывок записи прогона теста внимательно.
Вы заметили эти странные увеличения элементов navigationBar? Именно так работает аудит доступности. То есть во время прогона теста Xcode:
динамически изменяет размер шрифта на несколько категорий,
отслеживает каждый шаг,
анализирует полученные данные.
Так выглядит визуальная часть работы метода, но на самом деле аудит включает в себя проверку по следующим сценариям:
// Types of audits supported on all platforms
// Присутствуют ли на экране элементы нечитаемые из-за плохой контрастности с фоном?
public static var contrast: XCUIAccessibilityAuditType { get }
//Возможно ли обнаружить на экране элементы?
public static var elementDetection: XCUIAccessibilityAuditType { get }
//Соответствует ли область нажатия кнопки минимально-требуемому размеру 44x44?
public static var hitRegion: XCUIAccessibilityAuditType { get }
//Есть ли у элемента ёмкое описание, позволяющее пользователю понять назначение элемента?
public static var sufficientElementDescription: XCUIAccessibilityAuditType { get }
// Types of audits supported on iOS, watchOS, and tvOS
//Поддерживает ли элемент, содержащий текст, динамическое изменение шрифта?
public static var dynamicType: XCUIAccessibilityAuditType { get }
//Не будет ли текст обрезан (теоретически) при динамическом изменении шрифта или изменении верстки?
public static var textClipped: XCUIAccessibilityAuditType { get }
//Есть ли у элемента характеристика, позволяющая пользователю понять способ взаимодействия с элементом или его текущее состояние?
public static var trait: XCUIAccessibilityAuditType { get }
Далее рассмотрим, какие же замечания нашлись в нашем Example-проекте и попробуем их исправить. Пойдём от легких к сложным.
Отсутствующее описание (sufficientElementDescription)
Для начала отфильтруем сценарий аудита в коде теста, чтобы в отчёте не было лишних замечаний.
try app.performAccessibilityAudit(for: [.sufficientElementDescription])
Это пример аудита по одному сценарию. Получим отчёт, из которого будет ясно, что элемент без описания найден на трёх экранах проекта.
Провалимся в тест и обнаружим проблемный элемент — UISearchBar
.
Исправить ошибку можно, добавив accessibilityLabel
этому элементу. Например, так.
searchBar.searchTextField.accessibilityLabel = "Search field"
Интересно, что UISearchBar, будучи комплексным, но всё-таки системным элементом, недоступен по умолчанию. То есть его не видит VoiceOver и его нельзя однозначно идентифицировать через автотесты.
Учтите, что searchTextField доступен только с iOS 13. На более ранних версиях придется искать альтернативное решение.
Обрезанный текст (textClipped)
Рассмотрим несколько случаев срабатывания сценария textClipped
. Первый — с длинным заголовком в navigationBar
.
Вы скажете, что текст на этом элементе совсем не обрезан, и будете правы. Но если размер шрифта изменится, то...
или же в navigationBar
может быть добавлен дополнительный элемент справа.
Нехорошо. Скрывая часть текста, мы скрываем важный контекст. Следует подумать над другой формулировкой заголовка. Например,
title = "Table with alphabetize sections"
title = "alphabetize sections"
Это — лёгкий путь, если позволяют требования.
Второй случай — кривая вёрстка заголовка секции.
Стоит отметить что для отладки аудита можно также использовать accessibility inspector
— утилиту, встроенную в Xcode. В Xcode 15 она снова работает.
Такой способ отладки позволит получить описание предупреждения и советы по исправлению замечания. Например, Use flexible and stackable layouts, word wrap and hyphenate, or increase row heights. Дельный совет, который исправит ошибку в этом случае.
Текст статичного размера (dynamicType)
Пожалуй, самый зрелищный сценарий и самый трудоёмкий для исправления.
На предыдущих записях прогонов теста мы уже видели, что performAccessibilityAudit
меняет размер шрифта, но этот эффект был заметен только на UINavigationBar
. Почему? Спросим у нашего верного инспектора.
Иными словами, для поддержки dynamicType требуется, чтобы шрифт соответствовал категории из таблицы dynamic type sizes.
Задать категорию можно:
через xib,
методом
UIFont.preferredFont(forTextStyle: TextStyle)
в случае системного шрифта,
Используя
UIFontMetrics(forTextStyle: TextStyle).scaledFont(for: UIFont,..
в случае кастомного шрифта.
Во всех случаях надо исходить из заданного дизайнером размера шрифта и подбирать максимально близкую категорию из таблицы. От себя добавлю, что не лишним будет добавить estimatedSize
для ячеек и убрать все статические значения высоты. В этом случае следовать лишь совету инспектора недостаточно.
Вот так выглядит экран с поддержкой динамического изменения размера шрифта. Осталось исправить ещё 200 замечаний по сценарию dynamicType, и Example-проект будет полностью доступен.
Так уж повелось, что Apple в первую очередь думает о пользователях, а не о разработчиках. Однако на нынешней WWDC23 Apple удивила нас улучшениями в отчётах о тестировании и суперполезным методом для автоматического аудита доступности. Эти улучшения действительно упрощают отладку и написание тестов и повышают качество вашего продукта.
Надеюсь, наши советы по разрешению замечаний аудита доступности тоже будут вам полезны. Как говорилось на одной из сессий WWDC23, доступный продукт — это премиальный продукт.