-
읽기 앱의 접근성 향상하기
VoiceOver, 화면 말하기 등을 위한 강력한 읽기 경험을 선사하는 방법을 알아보세요. 직관적인 텍스트 선택, 줄과 단락 간의 명확한 탐색, 개별 요소와 여러 페이지에 걸친 끊김 없는 읽기를 제공하는 방법을 살펴보세요.
챕터
- 0:01 - Introduction
- 1:26 - Characteristics
- 3:45 - Standard views
- 14:05 - Custom text
리소스
- accessibilityNextTextNavigationElement
- editCategory
- accessibilityLinkedGroup(id:in:)
- causesPageTurn
- UITextInput
- Accessibility for UIKit
관련 비디오
WWDC19
-
비디오 검색…
안녕하세요!
저는 Josh이고, 소프트웨어 엔지니어입니다 접근성 팀 소속입니다. 오늘은 장문 텍스트나 독서 앱을 Apple 플랫폼에서 모든 사람이 접근할 수 있게 만드는 방법을 말씀드리겠습니다.
장문 콘텐츠를 읽는 것은 UI 탐색과 근본적으로 다릅니다. 텍스트를 유려하게 이동하는 것이고, 컨트롤과 같은 UI 요소 간 이동에만 그치지 않습니다.
Apple의 프레임워크에는 접근 가능한 텍스트가 기본으로 탑재되어 있습니다. 하지만 개발자로서 더 많은 작업을 통해 장문 텍스트의 접근성 경험을 풍부하고 확장할 수 있습니다.
오늘은 장문 콘텐츠를 구축할 때 고려할 몇 가지 모범 사례와 기술을 공유하겠습니다.
먼저, VoiceOver나 다른 보조 기술을 사용하는 사람에게 훌륭한 독서 경험을 구성하는 특성이 무엇인지 이야기하겠습니다. 그런 다음, UIKit과 SwiftUI의 뷰를 사용하고 확장하는 방법을 보여드리겠습니다 독서 경험을 위해 특별히 설계된 풍부한 API를 활용하여.
마지막으로, 앱의 커스텀 텍스트를 접근 가능하게 만드는 방법을 다루겠습니다 VoiceOver, Speak Screen, 또는 Accessibility Reader에서.
먼저 장문 콘텐츠를 표시하는 앱에서 훌륭한 접근성 경험이란 무엇인지 이야기하겠습니다. 오늘 저는 제가 추천하는 정보와 여행 팁을 공유할 수 있는 앱을 만들려고 합니다 제가 가장 좋아하는 도시 중 하나인 시카고에 대해서요.
제 앱은 여러 단락과 텍스트가 있는 페이지별 콘텐츠를 가지고 있으며 여러 줄에 걸쳐 이어집니다. 보조 기술을 사용하는 모든 사람이 이 앱에서 훌륭한 경험을 할 수 있도록 만들고 싶습니다. 이 세션에서는 두 가지 인기 있는 Apple 플랫폼에 내장된 보조 기술에 집중할 것입니다: VoiceOver와 Speak Screen. VoiceOver는 Apple의 내장 스크린 리더로, 시각 장애인이나 저시력자를 위해 설계되었습니다. 활성화하면 커서로 강조 표시된 내용을 들을 수 있습니다. 아침. 제목. 우리는 링컨 파크에서 아침을 시작했고, 산책로를 거닐며 시카고 스카이라인의 경치를 감상했습니다.
Speak Screen은 페이지의 모든 콘텐츠를 위에서 아래로 소리 내어 읽도록 설계되었으며, 말하는 동안 강조 표시합니다. 켜면 화면 상단에서 두 손가락으로 아래로 드래그하여 시작할 수 있습니다. 정오. 점심때 우리는 시카고 강을 따라 걸었습니다. 강변 산책로는 도시의 웅장한 건축물의 멋진 경치를 제공했습니다. 우리가 가장 좋아한 경치는 듀세이블 브리지 중간에서 본 것으로, 강을 따라 곧장 내려다볼 수 있었습니다.
이러한 기술들을 염두에 두고, 저는 이 기능들과 제 앱 간의 상호작용을 개선하기 위해 세 가지 목표를 세웠습니다. 구체적으로, 제 앱이 세밀한 텍스트 탐색을 제공하도록 하여 VoiceOver와 Speak Screen이 텍스트를 유려하게 이동할 수 있도록 하고 싶습니다. 또한 연속적인 독서 경험을 개발하도록 하여 보조 기술을 사용하는 사람이 중단 없이 사용할 수 있도록 하고 싶습니다. 마지막으로, 제 앱이 포괄적인 텍스트 선택을 제공하도록 하고 싶습니다.
이 비디오의 나머지 부분에서 이 주요 목표들에 집중하고, 제 여행 앱이 그 모두를 만족시키는지 확인하겠습니다.
Apple의 프레임워크는 많은 텍스트 컴포넌트를 제공하며 기본적으로 접근 가능하므로, 이제 그것이 무엇인지, 무엇을 제공하는지, 추가 기능으로 어떻게 확장할 수 있는지 집중하겠습니다. UIKit과 SwiftUI 모두 라인, 단어별로 접근 가능한 텍스트 뷰를 제공하며, VoiceOver와 Speak Screen을 통한 문자 탐색과 접근 가능한 텍스트 선택을 지원합니다. 이미 UIAccessibilityReadingContent에 익숙하실 수도 있는데, 전체 페이지 콘텐츠를 접근 가능하게 만드는 훌륭한 방법입니다. 저는 그 프로토콜에 집중하지 않을 것이지만, 오늘 제가 논의할 모든 것 위에 여전히 사용하고 채택할 수 있습니다. 더 자세히 알아보려면 "접근 가능한 독서 경험 만들기"를 확인하세요. 오늘은 UITextInput에 집중하겠습니다. 네이티브 텍스트 뷰가 사용하는 고해상도 프로토콜로, 커스텀 뷰에서도 채택할 수 있습니다.
시스템 전반의 표준 텍스트 뷰는 UITextInput 프로토콜을 채택합니다. UIKit에서 iOS의 UITextView를 사용하면 풍부한 텍스트 경험을 바로 제공받을 수 있으며, SwiftUI의 TextEditor도 마찬가지입니다. 선택이 활성화된 간단한 SwiftUI Text 뷰를 사용하여 모든 Apple 플랫폼에서 이러한 기능의 혜택을 누릴 수도 있습니다. macOS 앱을 개발하는 분들은 AppKit의 NSTextView를 사용하거나, 논의된 SwiftUI 뷰를 사용하면 이러한 이점을 얻을 수 있습니다. 앱의 제약 조건이 허용할 때는 항상 이러한 컴포넌트를 사용하도록 하세요.
제 여행 가이드 앱에서는 UITextView를 선택하여 접근 가능한 속성을 위해 각 개별 단락에 사용했습니다. 제가 설계한 고유한 레이아웃은 각 단락에 별도의 텍스트 뷰를 사용하도록 요구했으며, 여러 단락을 포함하는 하나의 뷰 대신. 먼저 세밀한 텍스트 탐색 제공이라는 목표에서 제가 어떻게 하고 있는지 평가하겠습니다.
VoiceOver에는 화면에서 손가락을 터치할 때 어떤 세밀도의 텍스트가 읽히는지 선택하는 설정이 있습니다. 저는 줄로 설정했으므로, VoiceOver가 켜진 상태에서 화면의 어느 줄이든 탭하면 그 줄이 소리 내어 읽힙니다. 우리는 링컨 파크에서 아침을 시작했고, 산책로를 거닐며 감상했습니다... VoiceOver는 이동 방식을 변경하는 옵션도 제공하는데, 로터라는 기능을 통해서입니다. 활성 로터는 두 손가락 회전 제스처를 사용하여 모드를 전환할 수 있습니다. 이제 그 제스처를 사용하여 줄 로터로 전환하고, 한 손가락으로 아래로 스와이프하여 페이지의 다음 줄을 찾겠습니다.
줄.
...시카고 스카이라인의 경치.
이제 이 단락의 끝에서 다음 단락의 첫 번째 줄로 이동해 보겠습니다.
현재는 각각이 단락들이 별도의 뷰이기 때문에, VoiceOver는 단락 내에서 줄 단위로 탐색하는 데 갇혀 있어 줄 단위로 전체 페이지를 탐색할 수 없으며 그것이 그 소리가 재생되는 이유입니다.
VoiceOver가 단락 간에 원활하게 이동할 수 있도록, iOS 18에서 텍스트 탐색 API가 도입되었습니다. 연결하려는 각 텍스트 요소에 대해, VoiceOver가 탐색해야 할 다음 및 이전 접근 가능한 텍스트 요소를 반환합니다.
예를 들어, 두 개의 단락 뷰가 있다면, 단락 1의 accessibilityNextTextNavigationElement 메서드에서 단락 2를 반환하고, 단락 2의 accessibilityPreviousTextNavigationElement에서 단락 1을 반환할 수 있습니다.
여기, 제 여행 가이드 앱의 페이지 컨트롤러가 있습니다. 설정 중 configureNavigationElements 코드 경로가 실행될 때, 해당하는 경우 각 방향에 적절한 탐색 요소를 설정합니다.
이제 구현을 완료했으므로, VoiceOver는 한 단락의 끝을 지나 다음 단락의 첫 번째 줄로 이동할 수 있습니다. 공원을 떠나기 전, 무료 동물원에 꼭 들렀습니다 모든 것을 구경하기 위해...
SwiftUI를 사용하고 있다면, iOS 27부터 시작하여, 여러 텍스트 요소를 연결하는 것은 accessibilityLinkedGroup 수정자를 사용하면 동일한 효과를 얻을 수 있습니다. 예를 들어, 여기에 동등한 페이지 뷰가 있으며 두 개의 선택 가능한 텍스트 요소가 있습니다. accessibilityLinkedGroup으로 둘 다 연결하면 동일한 id와 네임스페이스를 사용하여, 텍스트 탐색 동작을 얻게 됩니다. Mac에서 AppKit을 사용하고 있다면, accessibilitySharedTextUIElements를 확인하세요 비슷한 결과를 위해. 이제 VoiceOver가 다양한 텍스트 세밀도로 제 앱의 페이지를 탐색할 수 있으며, 예상치 못한 간격 없이 탐색할 수 있습니다. 하지만 제 앱의 연속적인 독서 경험이 최대한 부드럽도록 만들고자 합니다.
페이지별 콘텐츠는 본질적으로 페이지 간 스와이프를 필요로 합니다. 목표는 보조 기술이 이 콘텐츠와 원활하게 상호작용하도록 하여 페이지가 방해가 되지 않도록 하는 것입니다. VoiceOver와 Speak Screen 모두 누군가가 모든 콘텐츠를 읽을 수 있도록 처음부터 끝까지 스와이프 없이 할 수 있는 기능이 있습니다.
Speak Screen으로 현재 앱 경험을 살펴보겠습니다. 전체 읽기를 위해 두 손가락으로 화면 상단에서 아래로 스와이프하겠습니다. 정오. 점심때 우리는 시카고 강을 따라 걸었습니다. 강변 산책로는 도시의 웅장한 건축물의 멋진 경치를 제공했습니다. 우리가 가장 좋아한 경치는 듀세이블 브리지 중간에서 본 것으로, 강을 따라 곧장 내려다볼 수 있었습니다. Speak Screen이 읽기를 멈췄다는 것을 알아챌 것입니다 페이지 하단에 도달했을 때. 페이지별 콘텐츠에서 전체 읽기의 최상의 경험은 모든 페이지를 이동하여, 적절할 때 앞으로 넘어가는 것으로, 오디오북과 유사합니다.
여기 제 앱의 페이지 뷰 컨트롤러가 다시 있습니다. viewDidLoad 오버라이드에서, causesPageTurn 트레잇을 페이지의 마지막 단락에 적용할 수 있으며, 이는 UIKit과 SwiftUI 모두에서 사용 가능합니다. accessibilityScroll과 함께 사용하면, Speak Screen과 VoiceOver가 페이지 끝에 도달할 때 자동으로 페이지를 스크롤합니다.
마지막 단락에 해당 트레잇을 적용하여 Speak Screen을 사용해 보겠습니다.
정오. 점심때 우리는 시카고 강을 따라 걸었습니다. 강변 산책로는 도시의 웅장한 건축물의 멋진 경치를 제공했습니다. 우리가 가장 좋아한 경치는 듀세이블 브리지 중간에서 본 것으로, 강을 따라 곧장 내려다볼 수 있었습니다. 저녁. 하루를 마무리하며, 우리는 호숫가를 따라 걸었습니다 달리는 사람들과 자전거 이용자들과 함께. 이것은 스카이라인의 또 다른 멋진 경치를 선사해 주었으며, 미시간 호수의 물 위로 솟아 있었습니다.
훌륭합니다! Speak Screen이 읽기를 마치면 자동으로 다음 페이지로 포커스를 이동했으며, 제가 기대했던 대로입니다.
앞서 언급했듯이, 제가 확인하고 싶은 마지막 동작은 VoiceOver에서 텍스트 선택이 어떻게 작동하는지입니다. 제 앱에서 나중에 참조하기 위해 선택된 콘텐츠를 저장하는 기능을 추가했습니다 도구 모음의 버튼을 사용하여. 이 기능이 접근 가능한지 확인해야 합니다.
여기에 UITextView를 사용하고 있으며, 이미 접근 가능한 선택이 있습니다. SwiftUI에서 TextEditor를 사용하거나 선택이 활성화된 텍스트를 사용하면 같은 경험을 얻을 수 있습니다. 하지만 사람들이 할 수 있도록 하고 싶습니다 이 '추천 저장' 기능을 발견하도록 선택된 텍스트에 대해. 시각적으로는 이 버튼을 도구 모음에 추가했습니다 현재 선택을 저장하기 위해, 하지만 편집 로터를 통해 VoiceOver에 더욱 발견하기 쉽게 만들 수 있습니다. 이를 위해 커스텀 액션을 만들고 VoiceOver의 편집 로터에 추가할 수 있습니다 액션을 빌드할 때 편집 카테고리를 지정하여. 제 경우에는 accessibilityCustomActions를 오버라이드하겠습니다 단락 UITextView 서브클래스에서, 추천 저장 커스텀 액션을 추가하겠습니다 슈퍼 구현의 액션들과 함께. 텍스트 선택과 연관될 커스텀 액션이 있을 때는 편집 카테고리를 사용하세요 텍스트 선택과 연관된, 일반 액션이 아닌.
이제 VoiceOver를 켜서 시험해 보겠습니다. 텍스트를 선택하기 위해 텍스트 선택 로터로 전환하고, 단어 편집 모드로 전환하고, 오른쪽으로 스와이프하여 선택을 늘리겠습니다.
텍스트 선택. 오른쪽으로 스와이프하여 선택 확장. 왼쪽으로 스와이프하여 선택 축소. 단어 선택. "우리가 가장 좋아한 경치는 듀세이블 브리지에서..." 선택됨.
텍스트가 선택되면, 편집 로터로 전환하고, 선택 저장 액션을 활성화하여 저장하겠습니다.
줄. 단어. 문자. 편집.
선택 저장. 텍스트가 저장되었습니다. 훌륭합니다! 이제 시스템 텍스트 뷰를 사용하는 접근 가능한 앱 경험을 갖게 되었으며, API를 채택하여 새로운 접근 가능한 독서 기능 세트를 잠금 해제했습니다. 요소 간 줄 탐색, 연속 읽기, 텍스트 선택 모두 제가 기대한 대로 작동합니다. 가장 좋은 점은 VoiceOver와 Speak Screen이 이러한 변경에서 혜택을 받는 유일한 기술이 아니라는 것입니다. iOS 26부터 누군가가 Accessibility Reader를 열 수 있으며, 더 쉽게 소비할 수 있도록 텍스트 콘텐츠를 표시하도록 설계된 도구입니다. 제어 센터에 리더 제어를 추가했으므로, 그것을 누르면 제 앱의 콘텐츠가 Accessibility Reader에서 열립니다. 지금까지 공유한 것처럼 접근 가능한 텍스트 실천을 구현하면 콘텐츠에 대한 리더 경험도 더 좋아질 것입니다.
이것이 독서 콘텐츠를 위해 표준 텍스트 뷰를 접근 가능하게 만드는 방법이며, 항상 먼저 그러한 뷰를 선택할 것을 권장하지만, 모든 상황이 허용하지는 않습니다.
이제 커스텀 텍스트를 사용하고 있을 때 무엇을 해야 하는지에 집중하겠습니다, 또는 커스텀 텍스트 요소를, 접근 가능하게 만들기 위해.
커스텀 텍스트를 사용하는 것은 전용 독서 앱에서 흔히 볼 수 있는 패턴입니다 고급 타이포그래피를 지원하거나, 개발자의 애플리케이션 간에 코드를 공유하거나, 스캔된 페이지를 표시하기 위해. 여행할 때 저는 가는 곳에 대한 손으로 쓴 메모를 좋아합니다, 그래서 텍스트 뷰를 더 개인적인 느낌을 주기 위해 노트북에서 스캔한 페이지로 교체하기로 했습니다 더 개인적인 느낌을 주기 위해. 안타깝게도, 이것은 제가 UITextView가 무료로 제공했던 접근성 동작을 잃었다는 것을 의미합니다, 가장 기본적인 것을 포함하여: 텍스트 읽기. 아침. 제목. 이미지.
이 콘텐츠를 접근 가능하게 만드는 가장 좋은 방법은 UITextInput 프로토콜을 사용하는 것입니다, 어떤 접근성 요소에도 채택할 수 있습니다. 이 프로토콜은 렌더링된 텍스트나 이미지 내의 텍스트를, 예를 들어, 표준 텍스트 뷰에 있는 것처럼 접근 가능하게 만들 수 있습니다. UITextInput을 완전히 구현하면 네이티브 텍스트 뷰를 사용하는 것처럼 동일한 텍스트 경험을 얻을 수 있습니다. VoiceOver로 줄별 터치 탐색, VoiceOver 로터와 Speak Screen으로 세밀한 탐색, 텍스트 선택을 얻게 됩니다.
이 프로토콜을 구현하려면 몇 가지 문제를 해결해야 합니다. 텍스트의 기하학적 구조를 관리해야 하고, 주어진 범위에 대한 선택 사각형을 계산해야 합니다, 예를 들어 selectionRects 메서드에서.
보조 기술이 뷰에서 범위를 조회할 때, 텍스트의 해당 부분만 반환할 수 있어야 합니다. 그리고 중요하게도, 토크나이저를 제공해야 하며, 줄, 문장, 단어 또는 문자별 탐색을 관리하는 데 도움을 줍니다.
이것들은 구현해야 할 몇 가지 사항에 불과합니다. 이 프로토콜의 모든 접근성 이점을 얻으려면, 전체적으로 구현해야 합니다.
제 앱에서는 접근성 요소에 이 프로토콜을 구현했습니다 텍스트를 접근 가능하게 만들기 위해. 여기서 이 프로토콜에서 selectionRects 메서드를 구현했습니다, VoiceOver가 어떻게 다른 보조 기술이 제 콘텐츠를 '강조 표시'하는지 결정합니다. 이미지의 손글씨로 작업하고 있기 때문에, 각 줄의 알려진 높이와 너비를 사용할 수 있습니다 대략적인 사각형을 계산하기 위해 커스텀 함수인 selectionRectFromImage를 사용하여 주어진 범위에 대해. 이 정보를 사용하여 선택 사각형 배열을 구축하고, 이 메서드에서 반환합니다.
또한 나머지 구현을 완료하겠습니다 프로토콜의 나머지 메서드에 대해, textInRange에 대한 올바른 부분 문자열 가져오기와 토크나이저 제공 같은. 제 경우, UITextInputStringTokenizer를 서브클래싱했습니다 UIKit이 제공하는 커스텀 토크나이저를 만들기 위해 제 구현과 함께 작동하므로 그것을 반환하겠습니다.
마지막으로, 선택 경험이 선택 핸들과 강조 표시로 시각적으로 완성되도록 하고 싶습니다. 이를 위해 페이지 뷰에 UITextInteraction을 추가했으며, 선택이 변경될 때 입력 델리게이트를 호출하여, 시스템이 시각적 요소를 업데이트해야 함을 알립니다. 이것은 UITextInput 구현의 필수 부분은 아니지만, 표준 텍스트 뷰에서의 경험에 대한 기대에 맞게 제 앱을 완성합니다.
UITextInput은 causesPageTurn과 함께 훌륭하게 작동하며 탐색 요소 API와 함께, 제 앱의 새 버전에서도 이를 구현했습니다.
앱 업데이트를 완료했으며, 시간을 들여 신중하게 나머지 UITextInput 프로토콜을 구현하고 스캔된 텍스트에 필요한 모든 API를 구현했는지 확인했습니다. 이제 VoiceOver 경험을 확인해 보겠습니다.
먼저, 줄 단위로 탐색하겠습니다. 줄. 우리는 링컨 파크에서 아침을 시작했으며, 경치를 감상했습니다 시카고 스카이라인의. 동물원에는 많은 동물이 있었습니다.
이제 텍스트 선택 로터로 전환하여 텍스트를 선택하겠습니다 오른쪽으로 스와이프하여. 텍스트 선택. 오른쪽으로 스와이프하여 선택 확장. 왼쪽으로 스와이프하여 선택 축소. 줄 선택. "동물원에는 많은 동물이 있었습니다" 선택됨. 줄. 단어. 문자. 편집. 선택 저장. 텍스트가 저장되었습니다.
마지막으로, 전체 읽기를 시도할 수 있습니다. 아침. 제목. 우리는 링컨 파크에서 아침을 시작했으며, 시카고 스카이라인의 경치를 감상했습니다. 동물원에는 많은 동물이 있었습니다. 정오. 제목. 점심때 우리는 시카고 강을 따라 걸었습니다. 듀세이블 브리지에서의 경치는 사진 찍기에 완벽했습니다! 놀랍습니다! 모든 것이 원활하게 작동합니다. 이제 훌륭한 독서 경험을 만드는 것과 그것을 가능하게 하는 API를 다루었으니, 여러분 자신의 앱을 검토할 시간입니다.
VoiceOver를 켜고 앱을 감사하여 전체 읽기 제스처를 시도해 보고, 줄 로터를 사용하여 탐색하고, 텍스트를 선택해 보세요. 표준 텍스트 뷰를 사용하고 있다면, causesPageTurn 채택을 고려하세요 부드러운 페이지 간 독서 동작을 위한 텍스트 탐색 요소 API와 함께. 커스텀 렌더링된 텍스트를 사용한다면, UITextInput을 채택하세요. 이 작업을 통해 훌륭한 경험을 제공할 수 있습니다 앱을 다운로드하는 모든 사람에게.
-
-
7:29 - Link text elements together with navigation APIs
// Link text elements together with navigation APIs import UIKit class TravelGuidePageController: UIViewController { var paragraphs: [TravelGuideParagraph] func configureNavigationElements() { for (index, paragraph) in paragraphs.enumerated() { if index + 1 < paragraphs.count { paragraph.accessibilityNextTextNavigationElement = paragraphs[index + 1] } if index - 1 >= 0 { paragraph.accessibilityPreviousTextNavigationElement = paragraphs[index - 1] } } } } -
7:59 - Link text elements together with a linked group
// Link text elements together with a linked group import SwiftUI struct PageView : View { @Namespace private var pageNamespace var paragraphs: [String var pageNumber: Int var body: some View { Text(paragraphs[0]) .textSelection(.enabled) .accessibilityLinkedGroup(id: pageNumber, in: pageNamespace) Text(paragraphs[1]) .textSelection(.enabled) .accessibilityLinkedGroup(id: pageNumber, in: pageNamespace) } } -
9:50 - Turn pages automatically after reading
// Turn pages automatically after reading import UIKit class TravelGuidePageController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.lastParagraphView.accessibilityTraits.insert(.causesPageTurn) } override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool { moveToPage(direction) var scrollString = "Page \(currentPage) of \(pages.count)" UIAccessibility.post(notification: .pageScrolled, argument: scrollString) return true } } -
11:45 - Add actions to the editor rotor
// Add actions to the editor rotor import UIKit class TravelGuideParagraph: UITextView { override var accessibilityCustomActions: [UIAccessibilityCustomAction]? { get { let saveAction = UIAccessibilityCustomAction(name: "Save Recommendation") { _ in self.saveRecommendation() } saveAction.category = UIAccessibilityCustomAction.editCategory return (super.accessibilityCustomActions ?? []) + [saveAction] } set { } } private func saveRecommendation() -> Bool { ... return true } } -
16:10 - Adopt UITextInput
// Adopt UITextInput import UIKit class ScannedPage: UIView, UITextInput { override init(frame: CGRect) { super.init(frame: frame) let interaction = UITextInteraction(for: .nonEditable) interaction.textInput = self addInteraction(interaction) } func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { var rects: [UITextSelectionRect] = [] let startLine = lineIndex(for: range.start) let endLine = lineIndex(for: range.end) for line in startLine...endLine { let rect = selectionRectFromImage(for: range, in: line) rects.append(rect) } return rects } func text(in range: UITextRange) -> String? { let nsRange = nsRange(from: range) guard let range = Range(nsRange, in: scannedText) else { return nil } return String(scannedText[range]) } var tokenizer: any UITextInputTokenizer { CustomHandwritingTokenizer(textInput: self) } weak var inputDelegate: UITextInputDelegate? var selectedTextRange: UITextRange? { // Update visuals when assistive technologies change selection willSet { inputDelegate?.selectionWillChange(self) } didSet { inputDelegate?.selectionDidChange(self) } } }
-
-
- 0:01 - Introduction
What makes reading apps an accessibility challenge distinct from UI navigation, and what the session covers — the characteristics of a great reading experience, extending UIKit and SwiftUI text views, and making custom text accessible.
- 1:26 - Characteristics
Reading apps present unique accessibility challenges distinct from standard UI navigation, requiring fluid movement through text for technologies like VoiceOver and Speak Screen. This session covers three goals — granular navigation, continuous reading, and text selection — using UIKit, SwiftUI, and AppKit APIs.
- 3:45 - Standard views
UITextView, SwiftUI's TextEditor and selectable Text, and NSTextView on macOS all adopt UITextInput automatically, providing line, word, and character navigation and accessible text selection. The accessibilityNextTextNavigationElement and accessibilityPreviousTextNavigationElement APIs (and the new accessibilityLinkedGroup for SwiftUI) connect separate text elements so VoiceOver can move between them seamlessly, while the causesPageTurn trait provides page turning automatically during read-all gestures.
- 14:05 - Custom text
When using custom or custom-rendered text — such as scanned images — adopting the full UITextInput protocol gives VoiceOver and Speak Screen the same granular navigation and selection capabilities as native text views. This requires implementing text geometry methods like selectionRects(for:), a tokenizer, and text range methods, and can be paired with UITextInteraction for visible selection handles.