若要插入建議而不套用欄位中的值,請將完成輸入與提交分開處理。在 macOS 15 或更新版本中,SwiftUI 提供原生文字建議;使用 submitScope() 阻止祖先提交處理常式,並以明確按鈕執行最後動作。
本教學會在你自己的 Mac app 中建立一個小型標籤選擇器。輸入 Bl 時會顯示合成標籤,例如 Blue Tile。選擇建議會更新可編輯的草稿;點擊 Apply Label 則會將草稿複製到已套用的值。這項區分很重要:阻止 app 動作與攔截 Return 鍵命令是不同的需求。
選擇符合互動方式的 API
Apple 的 textInputSuggestions(_:) 修飾器會為文字欄位顯示選項。每個可操作的建議都需要 textInputCompletion(_:),以提供替換字串。這兩個 API 的文件都標示支援 macOS 15 及更新版本。沒有完成修飾器的建議可能會顯示,但無法透過點擊回應。Apple:textInputSuggestions(_:)、Apple:textInputCompletion(_:)
對於短標籤欄位,替換整份草稿是合適的。Apple 的建議文件說明了這種整欄替換方式。不要假設此 API 提供相對於插入點的 token 編輯器,或行內後綴預覽;這些功能需要更專門的編輯設計。Apple:textInputSuggestions(_:)
| 你的需求 | 合適的起點 |
|---|---|
| 在 macOS 15 或更新版本提供完整標籤 | SwiftUI 原生文字建議 |
| 阻止欄位呼叫祖先提交處理常式 | 在欄位上使用 submitScope() |
| 使用自訂建議版面,或支援較早的部署目標 | 由 app 管理的建議按鈕,用來更新草稿繫結 |
| 讓 Return 接受反白項目並攔截該命令 | 具備命令處理功能、由你管理的 AppKit 控制項 |
| 預覽後綴,或替換插入點所在的單字 | AppKit 文字完成與明確的完成範圍 |
最後兩列是根據文件所述的命令與完成 API 推導出的架構選擇。當互動需要比替換字串和提交邊界更多控制時,這些選擇就很實用。
建立完整的 SwiftUI 標籤選擇器
在 Xcode 中建立一個使用 SwiftUI 的 macOS App 專案,並使用包含 macOS 15 API 的 SDK。將部署目標設為 macOS 15 或更新版本。以以下單一原始碼檔案取代產生的 app 與 content-view 宣告,並確保目標中只保留一個 @main app 宣告。
此範例將 draft 和 appliedLabel 分開處理。它刻意採用簡單的本機比對規則:區分大小寫的前綴比對、不對空文字提供建議,也不顯示完全相符的建議。Swift 將 hasPrefix(_:) 記載為字串前綴比較;更廣泛的比對規則則應由應用程式自行決定。Swift:Strings and Characters
import SwiftUI
@main
struct LabelCompletionApp: App {
var body: some Scene {
WindowGroup {
LabelCompletionView()
}
}
}
struct LabelCompletionView: View {
private let labels = [
"Blue Tile",
"Blue Tray",
"Green Tile",
"Round Tray"
]
@State private var draft = ""
@State private var appliedLabel = ""
@State private var applyCount = 0
@FocusState private var fieldIsFocused: Bool
private var matches: [String] {
guard !draft.isEmpty else { return [] }
return labels.filter {
$0.hasPrefix(draft) && $0 != draft
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Choose a label")
.font(.headline)
TextField("Type Bl to see suggestions", text: $draft)
.textFieldStyle(.roundedBorder)
.focused($fieldIsFocused)
.textInputSuggestions {
ForEach(matches, id: \.self) { label in
Text(label)
.textInputCompletion(label)
}
}
.submitScope()
Button("Apply Label") {
appliedLabel = draft
applyCount += 1
}
.disabled(draft.isEmpty)
Text("Draft: \(draft)")
Text("Applied: \(appliedLabel)")
Text("Apply count: \(applyCount)")
}
.padding(24)
.frame(width: 420)
.onAppear {
fieldIsFocused = true
}
}
}
這是完整的 app 範例,包含範例標籤與狀態。此處已依據取得的 API 文件進行檢視,但因為無法使用 Swift 工具鏈與 macOS UI 執行環境,所以沒有在此編譯或執行。
為什麼選擇建議不會套用標籤
每個完成項都會提供一個字串給文字欄位,而該欄位的繫結指向 draft。只有 Apply Label 按鈕動作中的程式碼會指定 appliedLabel 或遞增 applyCount。因此,在此範例中更新草稿不會觸發最後動作。
欄位也使用了 submitScope()。Apple 將此修飾器記載為可阻止提交觸發傳遞到檢視階層中較高處的提交修飾器。若此元件之後嵌入包含祖先 onSubmit 的較大型表單,這點就很重要。Apple:submitScope(_:)
初始焦點由 @FocusState 設定。Apple 的文件指出,以程式方式指定其繫結值會將焦點移到相關檢視。此處的焦點只是編輯上的便利功能,不會控制標籤是否套用。Apple:FocusState
此範例刻意允許的行為
標籤清單提供的是建議,而非限制有效輸入。輸入 Square Tile 仍會啟用 Apply Label。如果你的介面只允許清單中的標籤,請明確修改驗證規則,例如只有草稿等於目錄中的項目時才啟用按鈕。不要默默將不符合的草稿替換成第一個建議。
此範例也會替換整個標籤。如果草稿包含多個字詞,選擇完成項就應視為選擇欄位的最終值。需要保留周圍文字的 token 編輯器則必須依據選取範圍進行替換。
理解「不提交」的意思
SwiftUI 的 onSubmit(of:_:) 文件指出,使用者按下 Return 時,文字欄位會觸發其提交動作。提交動作可以附加在個別檢視或較大的檢視階層上。如果自動完成選擇似乎觸發了意外的 app 動作,請分別檢查這兩處。Apple:onSubmit(of:_:)
submitScope() 會為祖先處理常式建立邊界。文件並未保證不會發生任何內部提交事件、編輯會持續進行,或每個鍵盤命令都會被攔截。同樣地,Apple 的完成修飾器文件說明的是文字替換,並非全面抑制 Return 鍵行為。選擇互動方式時請留意這些限制。Apple:submitScope(_:)、Apple:textInputCompletion(_:)
對於現有元件,請分別檢查以下路徑:
- 如果 Return 永遠不應套用欄位的值,請從欄位本身的
onSubmit移除任何最終應用程式動作。 - 在欄位上加入
submitScope(),以阻止祖先提交動作。 - 個別檢查按鈕的鍵盤快速鍵,尤其是明確設定的預設動作。
- 檢查任何變更觀察器或模型 setter,確認它們是否會在草稿變更時執行最後動作。
空白的本機 onSubmit 閉包並非文件所述、可取代提交邊界的方式。Apple 特別提供 submitScope() 來停止事件傳遞。如果 app 只應在沒有完成項作用中時於 Return 鍵提交,就需要額外狀態與命令處理來實作這項條件式鍵盤策略。Apple:onSubmit(of:_:)
當 Return 必須接受並攔截時使用 AppKit
對於由你管理的 NSTextField,AppKit 提供 control(_:textView:doCommandBy:)。它的文字檢視引數是控制項的欄位編輯器。委派物件可以處理繫結的命令並回傳 true;回傳 false 則表示該委派物件未處理此命令。這是文件記載、可用來自訂文字欄位命令的擴充點。Apple:control(_:textView:doCommandBy:)
自訂完成控制器可以透過此擴充點採用以下流程:
- 追蹤建議清單是否開啟,以及目前反白的是哪個項目。
- 當 Return 命令到達且有有效的反白項目時,將該項目插入草稿。
- 關閉建議清單並同步 app 的草稿模型。
- 回傳
true,表示控制器已處理此命令。 - 對控制器未處理的命令回傳
false。
這套流程是根據委派契約推導的實作設計,並非內建的自動完成策略。請另外決定清單關閉時 Return 的行為。如果你管理的是獨立的 NSTextView,對應的攔截點是 textView(_:doCommandBy:);Apple 指出,回傳 true 可防止文字檢視自行執行該命令。Apple:textView(_:doCommandBy:)
至於文字完成,NSTextView.insertCompletion(_:forPartialWordRange:movement:isFinal:) 支援替換與選取範圍變更。isFinal: false 會選取建議的後綴;isFinal: true 則會在完成的單字後收合選取範圍。此旗標描述的是完成狀態,而非應用程式提交。movement 引數指出完成導覽或選取是如何發生的。Apple:insertCompletion(_:forPartialWordRange:movement:isFinal:)
保留正確的文字範圍
AppKit 的 rangeForUserCompletion 會識別從部分單字開頭到插入點的範圍。請使用符合預期替換內容的範圍,並在編輯或插入點移動後重新計算。文字變更後,不要不加檢查地重用先前擷取的範圍。Apple:rangeForUserCompletion
橋接文字範圍時,不要以 Swift 的字元數取代 NSString 長度。Swift 文件說明,String 計算的是延伸字素叢集,而 NSString 長度計算的是 UTF-16 編碼單元。兩者的數值可能不同。SwiftUI 範例中的整欄繫結替換則完全不需要轉換範圍。Swift:Strings and Characters)
分別驗證插入與套用
在每個受支援的 macOS 版本上,使用可見的草稿、已套用值與計數器來檢查結果。以下是此範例的預期檢查項目,並非已回報的測試結果。
| 動作 | 預期可觀察結果 |
|---|---|
輸入 Bl |
草稿為 Bl;顯示符合的標籤建議 |
點擊 Blue Tile |
草稿變成 Blue Tile;已套用值與計數維持不變 |
| 編輯時按下 Return | app 不會套用草稿 |
點擊 Apply Label |
已套用值複製草稿;計數增加一次 |
| 之後編輯草稿 | 先前已套用的值仍會顯示 |
| 清除草稿 | 建議清單為空,且 Apply Label 已停用 |
輸入 bl |
依照範例的區分大小寫規則,沒有符合項目 |
也請測試在原生建議清單中以鍵盤選取項目,並在將欄位嵌入實際父檢視後重做這些檢查。Apple 指出,移動 macOS 視窗等事件可能會關閉原生建議介面,因此即使清單消失,草稿仍應能正常使用。Apple:textInputSuggestions(_:)
如果你的部署目標早於 macOS 15,請提供受可用性條件控管的原生檢視與替代方案,或全程使用 app 管理的建議按鈕。替代按鈕可以將標籤直接指定給草稿,並將最後動作留給 Apply Label。這樣便能維持草稿與套用動作的區隔,而鍵盤導覽與呈現方式則仍須由元件負責。
對於這個標籤選擇器任務,實用的邊界很明確:接受完成項會變更可編輯的草稿,只有明確的最後動作會變更已套用的標籤。使用 SwiftUI 的原生建議與 submit scope 來建立這道邊界;如果需求包含攔截 Return 本身,則改用 AppKit 命令處理。
來源
- textInputSuggestions(_:) — Apple
- textInputCompletion(_:) — Apple
- submitScope(_:) — Apple
- onSubmit(of:_:) — Apple
- FocusState — Apple
- control(_:textView:doCommandBy:) — Apple
- textView(_:doCommandBy:) — Apple
- insertCompletion(_:forPartialWordRange:movement:isFinal:) — Apple
- rangeForUserCompletion — Apple
- Strings and Characters — Swift.org