2025 年 7 月 5 日

iOS iPadOS macOS Swift SwiftUI&UI Frameworks visionOS WWDC WWDC25

基礎到進階:掌握 SwiftUI TextEditor 與 AttributedString 的強大整合

已複製到剪貼板


打破純文字編輯的限制

在建構現代化的筆記或食譜應用程式時,開發者常面臨一個戰略性的瓶頸:傳統 TextEditor 對於 String 的依賴限制了表達力。對於使用者而言,純文字過於單調,無法滿足強調重點或層次化內容的需求。

WWDC 2025:Cook up a rich text experience in SwiftUI with AttributedString
WWDC 2025:Cook up a rich text experience in SwiftUI with AttributedString

WWDC25 針對 SwiftUI 的 TextEditor 進行了重大升級,正式將 AttributedString 提升為一等公民。這不僅僅是增加了視覺格式,更標誌著從單純的字元序列轉向「語義化 Semantic UI」的演進。透過這種深度整合,開發者能以低摩擦的成本,將基礎的文字框轉化為系統級的 AttributedString 編輯器。接下來,我們將探討如何僅透過型別更換,就能解鎖強大的 AttributedString 功能。

核心升級:將 TextEditor 轉變為 AttributedString 編輯器

TextEditor 從純文字提升至 AttributedString 的過程極其順暢。開發者只需將狀態變數從 String 改為 AttributedString,即可實現無縫遷移並立即啟用系統級功能。

低摩擦的轉型路徑

TextEditor 綁定至 AttributedString 時,它會自動解鎖以下屬性支援:

  • 字元樣式:粗體、斜體、底線、刪除線、自定義字體與點數大小
  • 視覺細節:前景與背景顏色、字距(Kerning)、追蹤(Tracking)、基準線偏移
  • 現代輸入:完整支援 Genmoji 與表情符號
  • 段落樣式:包含行高(Line Height)、對齊方式(Text Alignment)以及基礎書寫方向(Base Writing Direction)
// 傳統 String 綁定
@State private var recipeText: String = "準備食材..."

// 升級為 AttributedString 以啟用富文本
@State private var recipeText: AttributedString = "準備食材..."

// View 實作保持簡潔
TextEditor(text: $recipeText)

專家分析:語義一致性與環境替代

SwiftUI 的 FontColor 屬性具有強烈的語義特質。這對於適應動態字級(Dynamic Type)與深色模式至關重要。值得注意的是,TextEditor 會自動處理屬性值為 nil 的情況:當特定屬性未定義時,系統會從環境(Environment)中提取預設值進行替代。這種設計確保了 TextEditor 與不可編輯的 Text 元件在視覺呈現上保持高度一致。

處理複雜選擇:為什麼 RangeSet 是現代開發的必備知識

在全域化應用中,傳統的單一 Range 模型在處理雙向文本(Bidirectional Text)時會失效。這正是 RangeSet 成為現代 iOS 開發必備知識的原因。

當使用者在同一行中混合選取由左至右(LTR,如英文)與由右至左(RTL,如希伯來文)的文字時,視覺上的選取區塊雖然是連續的,但由於 AttributedString 在底層是以一致的邏輯順序(Logical Ordering)儲存,這些字元的索引實際上是不連續的。

為了精確對應使用者的意圖,SwiftUI 的 AttributedTextSelection 採用了 RangeSet。這讓開發者能夠切片出「不連續子字串 Discontiguous Substring」,確保無論語言環境如何複雜,資料的操作都能精準回饋到正確的字元上。

索引安全與變更轉型:避免 App 崩潰的關鍵

開發 AttributedString 編輯器時,最棘手的問題莫過於「索引失效」。AttributedString 為了優化效能並避免在變更時進行大規模的資料複製,底層採用了樹狀結構(Tree Structure)。索引(Index)實際上是儲存在這棵樹中的「路徑」。

為什麼會發生崩潰或游標跳轉?

當文字發生任何微小的變更(例如插入一個字元),整棵樹的佈局就可能重組,導致舊的路徑(索引)全數失效。如果開發者嘗試使用「過期」的索引來更新文字,SwiftUI 為了防止應用程式崩潰,會偵測到無效索引並強制將選取範圍移動到文件末尾。

使用 transform(updating:) 進行安全變更

為了維持語義的一致性,開發者應使用 transform API。它接受一個閉包,允許在其中對字串進行就地(In-place)修改,並在修改完成後自動重新計算索引路徑。

// 安全地更新文字並保持選取範圍正確
$recipeText.transform(updating: $selection) { proxy in
    // 在此執行變更,系統會自動重繪路徑
    // 即使文字位移,selection 仍會指向正確的語義位置
}

自定義格式定義與價值約束:建構專業級編輯體驗

在專業的食譜編輯器中,我們不希望使用者隨意修改字體或顏色,因為這可能破壞 UI 的一致性。例如,若使用者將文字設為白色,在白色背景下「牛奶 Milk」這個關鍵字就會消失。

聲明式約束的威力

透過 AttributedTextFormattingDefinition 協定,開發者可以限制編輯器的屬性範圍(Scope)。如果你在定義中排除了某些屬性,TextEditor 會自動調整系統格式選單,隱藏不相關的控制項。

更強大的工具是 AttributedTextValueConstraint。以「成分標註」為例,我們可以實作 IngredientsAreGreen 約束:

  1. 自動化邏輯:如果一段文字被標記為「成分」,則強制其顏色為綠色;否則設為 nil(回復系統預設)
  2. 主動探測 Probing:TextEditor 會自動探測這些約束。當開發者選取已標記為成分的文字時,系統會發現任何顏色修改都會被約束改回綠色,因此會自動禁用顏色選擇器

這種方式不僅減輕了同步 UI 狀態的負擔,更能確保在內容貼上(Paste)時,格式依然符合業務邏輯。

精細控制:屬性的繼承、失效與邊界

為了精確控制編輯行為,AttributedStringKey 提供了三個進階配置介面:

  1. inheritedByAddedText決定新輸入的文字是否繼承前一個字元的屬性。以「拼字檢查 Spell Checking」為例,紅色虛線屬性不應被新輸入的文字繼承,否則使用者輸入的每個字都會帶有錯誤標籤
  2. invalidationConditions定義屬性失效的時機。例如,當文字內容變更(textChanged)時,原有的拼字檢查結果應立即失效,以避免顯示過時的錯誤資訊
  3. runBoundaries用於強制屬性的一致性範圍。典型的應用是「段落對齊」,若此屬性設定為 paragraph 邊界,則即使使用者只選取一個單字進行對齊,系統也會自動將屬性擴散至整個段落

讓文字處理更具深度

WWDC25 為 SwiftUI 帶來的 TextEditorAttributedString 整合,大幅降低了 AttributedString 開發的門檻。透過這些語義化工具,開發者不再只是處理字元,而是在建構具備業務邏輯的內容。

在實踐這些技術時,建議進一步探索以下進階主題:

  • Swift Data 持久化:輕鬆儲存帶有複雜屬性的 AttributedString
  • Transferable Wrapper:實作跨應用程式的 Lossless(無損)拖拉支援(如匯出為 RTFD)
  • Genmoji 整合:為應用程式注入現代化的互動體驗

既然 SwiftUI 已經提供了如此強大的語義化基礎,你的下一個應用程式將如何重新定義文字與使用者的互動體驗?

分享文章

已複製到剪貼板

追蹤網站

透過 Google 追蹤

超級感謝

關於 XcodeProject

XcodeProject 創立於 2023,致力於協助開發者探索 Apple 的創新世界,學習在 iOS、iPadOS、macOS、tvOS、visionOS 與 watchOS 上開發 App,發現眾多技術與框架,讓開發者獲得更多能力。


Contacts

Ricky Chuang

XcodeProject

RickyChuang.xcodeproj@gmail.com

XcodeProject 聯絡

contact.xcodeproj@gmail.com

XcodeProject 的最新文章