iOS 27 的無障礙設計:閱讀類 App 與自訂控制項
閱讀長篇內容與在介面中導覽是截然不同的問題:前者的目標是在文字間流暢移動,而非在控制項之間跳轉。WWDC26 的兩場無障礙議程正是沿著這道接縫一分為二:一場談閱讀介面,一場談環繞其周圍的控制項。
這樣的劃分很重要,因為兩者的修正方式在本質上就不同。閱讀類 App 的問題出在連續性:文字無法跨段落銜接、朗讀整篇時停在頁面底部。自訂控制項的問題則出在轉譯:某個手勢在視覺上傳達了一切,卻沒能把任何訊息傳給 VoiceOver。iOS 27 同時為這兩種情況推出了 API,其中 accessibilityLinkedGroup 是今年全新登場的。
TL;DR
- 閱讀類 App 應優先選用系統文字檢視。啟用選取功能的
UITextView、TextEditor與Text都採用了UITextInput協定,可免費獲得逐行/逐字/逐字元的導覽與選取功能1。 - 當版面配置迫使文字元素分開時,請將它們連結起來,讓 VoiceOver 能跨越接縫移動。iOS 18 引入了
accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement;iOS 27 則新增了 SwiftUI 的accessibilityLinkedGroup修飾符來達成相同效果1。 - 對於分頁內容,
causesPageTurn特徵搭配accessibilityScroll,能讓「朗讀螢幕」與 VoiceOver 在朗讀整篇時自動翻頁1。 - 自行算繪的文字(掃描頁面、進階排版)會喪失上述所有功能。完整採用
UITextInput即可將其復原:透過selectionRects提供幾何資訊、透過textInRange提供子字串,並以 tokenizer 處理逐行/逐字/逐字元的導覽1。 - 自訂控制項遵循四項指導原則:用途、數值、動作、回饋。對應的工具為
accessibilityLabel/accessibilityValue、搭配accessibilityAdjustableAction的.adjustable特徵、用於多軸控制項的自訂動作,以及用於手勢密集介面的直接觸控(allowsDirectInteraction)2。
閱讀類 App:銜接被版面拆散的文字
這場談閱讀類 App 的議程,圍繞著一個看似簡單的限制條件展開。講者的旅遊指南 App 因版面需求,為每個段落使用各自獨立的 UITextView,而非以單一檢視容納整個頁面1。每個獨立的文字檢視本身都具備無障礙能力,問題卻出現在它們彼此的交界處。
講者為這款 App 設定了三個目標:細緻的文字導覽,讓 VoiceOver 與「朗讀螢幕」能流暢地穿行於文字之間;不受打斷的連續閱讀體驗;以及完整的文字選取1。議程的其餘部分,便是逐一檢視哪個 API 能滿足哪個目標。
要跨越獨立檢視進行導覽,答案是 iOS 18 引入的文字導覽元素 API。針對每個文字元素,你要回傳 VoiceOver 接下來應移往的下一個與上一個無障礙文字元素。在議程的範例中,段落 1 透過自身的 accessibilityNextTextNavigationElement 回傳段落 2,段落 2 則透過自身的 accessibilityPreviousTextNavigationElement 回傳段落 11。一旦接好線,VoiceOver 就會越過某段的結尾、進入下一段的第一行,而不再播放走到死路的提示音。
iOS 27 的新增功能落在 SwiftUI。如講者所言,從 iOS 27 開始,使用 accessibilityLinkedGroup 修飾符將多個文字元素連結在一起,即可達成相同效果1。你為連結的元素賦予相同的 id 與 namespace,它們便會繼承跨元素的文字導覽行為,無需手動維護下一個/上一個的對應關係。AppKit 則提供對應的 accessibilitySharedTextUIElements,在 Mac 上取得相同結果1。本系列的〈SwiftUI 的組成〉一文,說明了像這樣的 SwiftUI 修飾符如何向下解析到底層的無障礙樹。
連續性是第二個目標。分頁內容需要滑動,而朗讀整篇時應像有聲書一樣忽略頁面邊界。在議程中,「朗讀螢幕」在第一頁底部戛然而止,直到講者為每一頁的最後一個段落套用 causesPageTurn 特徵。搭配 accessibilityScroll 後,「朗讀螢幕」與 VoiceOver 一抵達結尾便會自動捲動到下一頁;此特徵在 UIKit 與 SwiftUI 中皆可使用1。
第三個目標——選取——在使用系統文字檢視時大多能自動取得,但議程加入了一個用心的巧思:一個透過 VoiceOver 編輯轉子提供的「儲存推薦」動作。講者覆寫了段落文字檢視的 accessibilityCustomActions,並以 edit 類別建立此自訂動作,正是為了讓它出現在編輯轉子中、與文字選取操作並列,而非淪為一般動作1。指引相當明確:當自訂動作與文字選取相關時,請使用 edit 類別。
當你自行算繪文字時:完整的 UITextInput
閱讀議程的後半段,處理的是系統檢視不可行的情況。自行算繪的文字會出現在講求進階排版的專業閱讀類 App、跨 App 共用的程式碼,或掃描頁面之中,而講者的範例最為極端:他把旅遊指南的文字檢視,換成從手寫筆記本掃描進來的頁面。代價是全面性的。改用影像後,便喪失了 UITextView 免費提供的無障礙行為,連最基本的——把文字朗讀出來——都不剩。VoiceOver 只會說「影像」1。
解法是採用 UITextInput 協定,它可附加在任何無障礙元素上,讓算繪文字或影像中的文字,變得跟標準文字檢視一樣無障礙1。議程直言其中的關鍵:你必須完整實作它,才能獲得完整的好處。講者逐一講解了幾個關鍵環節:
- 幾何資訊。
selectionRects為指定範圍計算反白矩形。講者以手寫影像為基礎,利用每一行已知的高度與寬度,透過自訂的selectionRectFromImage函式近似算出矩形,再回傳組合好的陣列1。 - 子字串。
textInRange只回傳輔助技術所查詢的那一段文字1。 - tokenizer。 逐行、逐句、逐字或逐字元的導覽,皆透過 tokenizer 處理。議程將 UIKit 的
UITextInputStringTokenizer子類別化,以配合自訂的版面配置1。
有一項細節調整明確屬於選用。為了讓選取帶有控點與反白、感覺更完整,講者為頁面檢視加上一個 UITextInteraction,並在選取變動時呼叫輸入委派,好讓系統更新視覺呈現。議程指出,UITextInput 本身並不要求這個步驟;它只是把體驗修飾得更圓滿,以貼近標準文字檢視1。而且 UITextInput 可與先前的 API 組合,因此 causesPageTurn 與導覽元素在自行算繪的文字上同樣有效。
議程點出了一項容易被低估的回報:做這些工作不只服務 VoiceOver 與「朗讀螢幕」。自 iOS 26 起,無障礙閱讀器能以更利於閱讀的版面開啟 App 的內容,而同樣這套無障礙文字的做法,也能改善那種體驗1。
自訂控制項:用途、數值、動作、回饋
這場談自訂控制項的議程,以一個標準的 SwiftUI 滑桿開場,並論證它為何行得通。你一眼就能讀出一條軌道、一個位於中間的把手、一個可拖曳的暗示,以及即時的回饋。沒有人解釋過其中任何一項。接著議程提出顯而易見的問題:要是有人看不到螢幕呢?VoiceOver 的回應是朗讀「亮度,50%,可調整」,再加上向上或向下滑動的提示——這傳達了視覺上呈現的同樣四件事:用途、數值、可用的動作,以及數值變動時的回饋2。
.adjustable 特徵,把它從乾巴巴的「按鈕,6 盎司」變成 VoiceOver 可操控的可調整滑桿。
這四個詞(用途、數值、動作、回饋)就是議程的指導原則,每個範例都會回扣到它們2。第一個是咖啡分配器控制項:向上拖曳出更多咖啡、向下拖曳出更少,填充程度代表盎司數。在任何處理之前,VoiceOver 只把它讀成一般的「按鈕,6 盎司」,完全沒提示該如何改變數值2。修正是逐步進行的:
- 用途與數值。
accessibilityLabel將它命名為「咖啡分配器」;accessibilityValue則播報目前的填充量2。 - 動作。
.adjustable特徵告訴 VoiceOver 此控制項會回應上/下滑動,而accessibilityAdjustableAction提供一個帶有方向參數(.increment或.decrement)的閉包,以處理各自的情況2。
這樣便能一次調整一盎司。為了更細緻的控制,議程動用了 VoiceOver 內建的穿透手勢:一個從控制項 accessibilityActivationPoint 起始的雙點按並按住,並在手指移動時把觸控事件直接送往控制項。講者把啟用點設定為對應目前的填充程度2。穿透期間的回饋是一堂談克制的小課:議程只在數值確實有所變動、且至少經過 0.3 秒時才發出播報,因為每次變動都播報會太吵2。
等化器面板則把標準拉得更高。它是二維控制項,議程坦言 .adjustable 並非合適的工具,因為它的遞增/遞減動作只涵蓋單一軸向。答案是自訂動作:將 accessibilityAction 修飾符套用四次,分別對應「上移」「右移」「下移」「左移」,每次把一個軸向推移固定的一步,並夾限在範圍之內2。與可調整動作不同的是,自訂動作支援你所定義的任何操作,而且也能服務切換控制與語音控制的使用者2。
直接觸控:當手勢本身就是重點
議程的最後一個範例是一個虛擬貓咪控制項,你可以撫摸、輕點與捏掐以觸發不同反應。講者指出,穿透在此並不合適,因為人們可能想一再重複某個動作、或使用多種手勢2。於是這個控制項改採直接觸控。
.requiresActivation 的 .accessibilityDirectTouch 修飾符,讓觸控直接穿透到貓咪,而不被 VoiceOver 攔截。
allowsDirectInteraction 特徵會把某個區域標示為直接觸控區:觸控事件直接送往控制項,而不經 VoiceOver 處理,因此控制項所支援的每一個手勢都能運作2。有兩個選項可塑造其行為。.requiresActivation 會讓控制項在雙點按之前維持靜止,使人能在螢幕上拖曳而不會誤觸;此後直接觸控會一直生效,直到焦點離開該元素為止。.silentOnTouch 則讓 VoiceOver 在該區域保持安靜,適用於那些會自行發出聲音、否則會被 VoiceOver 語音蓋過的控制項2。這隻虛擬貓咪使用的是搭配 .requiresActivation 的 .accessibilityDirectTouch2。
議程以一則告誡作結,呼應〈無障礙即平台〉中關於平台無障礙的論點:並非人人都能執行直接觸控手勢,因此請盡可能提供另一條途徑,例如自訂動作,讓切換控制與語音控制的使用者也能觸及同樣的互動2。
採用指引
兩場議程都以同一句叮囑收尾:打開 VoiceOver,親自檢視你自己的 App。具體而言:
- 對於建立在系統文字檢視上的閱讀介面,試試朗讀整篇的手勢、以行轉子導覽,並選取文字。若朗讀整篇停在頁面邊界,請採用
causesPageTurn搭配accessibilityScroll。若逐行導覽在獨立的文字元素之間走進死路,請以導覽元素 API(UIKit)或accessibilityLinkedGroup(SwiftUI,iOS 27)將它們連結起來1。 - 如果你自行算繪文字,請規劃完整採用
UITextInput,而非局部採用;這個協定是全有或全無,而選用的UITextInteraction步驟,正是讓選取感覺起來像原生的關鍵1。 - 對於任何自訂控制項,依序走過這四項原則。VoiceOver 使用者能不能辨別它是什麼(標籤)、它處於什麼狀態(數值)、他們能做什麼(adjustable 特徵或自訂動作),以及發生了什麼(播報)?把直接觸控保留給那些「數值即手勢本身」的控制項,並為它搭配一條非手勢的備援途徑2。
反覆出現的主題是:先選用系統元件,並把自訂路線視為需要紮實、完整投入的例外。〈一款 iOS App 的三個介面〉一文,把無障礙設計定位為與可見介面、App Intents 並列的一級介面;這兩場議程,正展現了把這個介面在文字與控制項上做對,是什麼模樣。
常見問題
iOS 27 為閱讀類 App 推出的新無障礙 API 是什麼?
SwiftUI 的 accessibilityLinkedGroup 修飾符。從 iOS 27 開始,將多個文字元素以相同的 id 與 namespace 連結起來,便能賦予它們跨元素的文字導覽,讓 VoiceOver 從某個元素的最後一行移往下一個元素的第一行。它是 iOS 18 的 accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement API,以及 AppKit accessibilitySharedTextUIElements 在 SwiftUI 中的對應做法1。
如果我使用標準文字檢視,還需要實作 UITextInput 嗎?
不需要。UITextView(UIKit)、TextEditor 與啟用選取的 Text(SwiftUI),以及 NSTextView(AppKit)都已採用 UITextInput,開箱即提供逐行/逐字/逐字元的導覽與選取。只有當你自行算繪文字——例如掃描頁面或進階排版——導致那些系統行為喪失時,才需要自己採用 UITextInput1。
自訂控制項何時該用 adjustable 特徵、何時該用自訂動作?
對於遞增與遞減有意義的單軸數值,例如滑桿,請使用搭配 accessibilityAdjustableAction 的 .adjustable 特徵。當單一軸向不夠用時,例如二維面板,或當你想公開讓 VoiceOver 逐一念出名稱的離散操作時,請使用自訂動作(accessibilityAction 修飾符)。議程的等化器面板之所以使用四個自訂動作(上/右/下/左移),正是因為 adjustable 特徵只涵蓋一個方向2。
什麼是直接觸控?我何時該使用它?
直接觸控(allowsDirectInteraction 特徵,在 SwiftUI 中透過 .accessibilityDirectTouch 套用)會把某個區域標示成讓觸控直接送往你的控制項,而不經 VoiceOver 處理,使人能使用控制項所支援的每一個手勢。請在穿透手勢並不合適的手勢密集控制項上使用它,並搭配 .requiresActivation 以防止誤觸。對於無法執行直接觸控手勢的人,請務必提供一條非手勢的備援途徑,例如自訂動作2。
無障礙文字如何嘉惠 VoiceOver 以外的功能?
同樣的工作也能在「朗讀螢幕」上回收成效;而且自 iOS 26 起,無障礙閱讀器能以更利於閱讀的版面開啟 App 的內容。實作閱讀議程所涵蓋的文字導覽、翻頁與 UITextInput 做法,只需一套改動,便能同時改善這三種體驗1。
延伸閱讀
- 無障礙即平台:Personal Voice、即時語音、眼動追蹤、音樂觸覺回饋
- SwiftUI 的組成
- 一款 iOS App 的三個介面
- iOS 26 的 Widget 與控制項介面
- 系列主頁:Apple Ecosystem 系列
- 指南:iOS Agent 開發
參考資料
-
Apple,WWDC26 議程 219,”Enhance the accessibility of your reading app.”。developer.apple.com/videos/play/wwdc2026/219。系統文字檢視(
UITextView、TextEditor、啟用選取的Text、NSTextView)採用UITextInput;iOS 18 的accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElementAPI 與 iOS 27 SwiftUI 的accessibilityLinkedGroup修飾符(AppKit:accessibilitySharedTextUIElements);causesPageTurn搭配accessibilityScroll;透過帶 edit 類別的accessibilityCustomActions提供的文字選取動作;為自行算繪文字完整採用UITextInput(selectionRects、textInRange、UITextInputStringTokenizer)及選用的UITextInteraction;以及 iOS 26 無障礙閱讀器,皆以此為出處。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple,WWDC26 議程 220,”Refine accessibility for custom controls.”。developer.apple.com/videos/play/wwdc2026/220。用途/數值/動作/回饋原則;使用
accessibilityLabel、accessibilityValue、.adjustable特徵與accessibilityAdjustableAction的咖啡分配器控制項;在accessibilityActivationPoint上、帶節流播報(數值變動加上經過 0.3 秒)的穿透手勢;用於等化器面板的accessibilityAction修飾符;以及為虛擬貓咪控制項透過allowsDirectInteraction(.accessibilityDirectTouch)搭配.requiresActivation與.silentOnTouch實現的直接觸控,連同非手勢備援途徑的提醒,皆以此為出處。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩