필드의 값을 적용하지 않고 제안을 삽입하려면 자동 완성과 제출을 분리하세요. macOS 15 이상에서 SwiftUI는 기본 텍스트 제안을 제공합니다. 상위 제출 핸들러를 차단하려면 submitScope()를 사용하고, 최종 작업에는 별도의 버튼을 사용하세요.
이 튜토리얼에서는 자체 Mac 앱에 간단한 레이블 선택기를 만듭니다. Bl을 입력하면 Blue Tile 같은 샘플 레이블이 표시됩니다. 제안을 선택하면 편집 중인 초안이 업데이트되고, Apply Label을 클릭하면 해당 초안이 적용된 값에 복사됩니다. 이 차이는 중요합니다. 앱 작업을 막는 것과 Return 키 명령을 처리하는 것은 서로 다른 요구 사항입니다.
상호작용에 맞는 API 선택하기
Apple의 textInputSuggestions(_:) 수정자는 텍스트 필드에 선택 항목을 표시합니다. 선택 가능한 각 제안에는 대체 문자열을 제공하는 textInputCompletion(_:)이 필요합니다. 두 API 모두 macOS 15 이상에서 사용할 수 있도록 문서화되어 있습니다. 자동 완성 수정자가 없는 제안은 표시될 수 있지만 클릭해도 반응하지 않습니다. Apple: textInputSuggestions(_:), Apple: textInputCompletion(_:)
짧은 레이블 필드에서는 초안 전체를 바꾸는 것이 적절합니다. Apple의 제안 문서에서는 필드 전체를 대체한다고 설명합니다. 이 API가 캐럿 기준 토큰 편집기나 인라인 접미사 미리보기를 제공한다고 가정하지 마세요. 이러한 기능에는 더 구체적인 편집 설계가 필요합니다. Apple: textInputSuggestions(_:)
| 요구 사항 | 적합한 출발점 |
|---|---|
| macOS 15 이상에서 완성된 레이블 제안 | SwiftUI 기본 텍스트 제안 |
| 필드가 상위 제출 핸들러를 호출하지 못하게 하기 | 필드에 submitScope() 적용 |
| 사용자 지정 제안 레이아웃을 사용하거나 이전 배포 대상 지원 | 초안 바인딩을 업데이트하는 앱 자체 제안 버튼 |
| Return으로 강조된 항목을 선택하고 해당 명령 처리 | 명령 처리를 갖춘 앱 소유 AppKit 컨트롤 |
| 접미사를 미리 보거나 삽입 지점의 단어를 대체 | 명시적 자동 완성 범위를 사용하는 AppKit 텍스트 자동 완성 |
마지막 두 항목은 문서화된 명령 및 자동 완성 API를 바탕으로 한 아키텍처 선택입니다. 대체 문자열과 제출 경계보다 더 세밀한 제어가 필요한 상호작용에 유용합니다.
완전한 SwiftUI 레이블 선택기 만들기
Xcode에서 SwiftUI를 사용하는 macOS 앱 프로젝트를 만들고 macOS 15 API가 포함된 SDK를 사용하세요. 배포 대상을 macOS 15 이상으로 설정하세요. 생성된 앱 및 콘텐츠 뷰 선언을 아래의 단일 소스 파일로 바꾸고, 타깃에는 @main 앱 선언을 하나만 남기세요.
이 예제는 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
}
}
}
샘플 레이블과 상태를 포함한 완전한 앱 예제입니다. 검색한 API 문서를 대조해 검토했지만 Swift 툴체인과 macOS UI 런타임을 사용할 수 없어 여기서 컴파일하거나 실행하지는 않았습니다.
제안 선택이 레이블을 적용하지 않는 이유
각 자동 완성 항목은 텍스트 필드에 문자열을 제공하며, 필드 바인딩은 draft를 가리킵니다. appliedLabel을 할당하거나 applyCount을 증가시키는 코드는 Apply Label 버튼 동작뿐입니다. 따라서 이 예제에서는 초안을 업데이트해도 최종 작업으로 이어지지 않습니다.
필드에는 submitScope()도 적용되어 있습니다. Apple은 이 수정자가 제출 트리거가 뷰 계층 상위의 제출 수정자에 도달하지 못하게 한다고 설명합니다. 나중에 이 컴포넌트를 상위 onSubmit이 있는 더 큰 양식에 넣을 때 중요합니다. Apple: submitScope(_:)
초기 포커스는 @FocusState에서 설정합니다. Apple 문서에 따르면 바인딩된 값을 프로그래밍 방식으로 지정하면 연결된 뷰로 포커스가 이동합니다. 여기서 포커스는 편집 편의를 위한 것이며, 레이블 적용 여부를 제어하지 않습니다. Apple: FocusState
예제에서 의도적으로 허용하는 동작
레이블 목록은 유효한 입력을 제한하지 않고 제안을 제공합니다. Square Tile을 입력해도 Apply Label 버튼이 활성화됩니다. 인터페이스에서 목록에 있는 레이블만 허용하려면 유효성 검사 규칙을 명시적으로 변경하세요. 예를 들어 초안이 카탈로그 항목과 일치할 때만 버튼을 활성화할 수 있습니다. 일치하지 않는 초안을 첫 번째 제안으로 조용히 바꾸지 마세요.
이 예제는 레이블 전체도 대체합니다. 초안에 여러 단어가 들어 있으면 자동 완성을 선택하는 것은 필드 전체의 완성된 값을 선택하는 것으로 이해해야 합니다. 주변 텍스트를 보존해야 하는 토큰 편집기에는 선택 영역을 고려한 대체가 필요합니다.
“제출하지 않음”의 의미 이해하기
SwiftUI의 onSubmit(of:_:) 문서에서는 사용자가 Return을 누르면 텍스트 필드가 제출 동작을 실행한다고 설명합니다. 제출 동작은 개별 뷰나 더 큰 계층에 연결할 수 있습니다. 자동 완성 선택이 예상치 못한 앱 작업을 실행하는 것처럼 보이면 두 위치를 모두 확인하세요. Apple: onSubmit(of:_:)
submitScope()은 상위 핸들러를 위한 경계를 설정합니다. 내부 제출 이벤트가 발생하지 않는다는 보장, 편집이 계속 활성 상태라는 보장, 모든 키보드 명령이 처리된다는 보장을 문서화하지는 않습니다. 마찬가지로 Apple의 자동 완성 수정자는 텍스트 대체를 설명하며 Return 키 동작을 보편적으로 억제한다고 설명하지는 않습니다. 상호작용을 선택할 때 이러한 한계를 고려하세요. Apple: submitScope(_:), Apple: textInputCompletion(_:)
기존 컴포넌트에서는 다음 경로를 각각 확인하세요.
- Return 키가 값을 적용하지 않도록 하려면 필드 자체의
onSubmit에서 최종 앱 작업을 제거합니다. - 상위 제출 작업을 차단하려면 필드에
submitScope()를 적용합니다. - 버튼 키보드 단축키를 별도로 확인합니다. 특히 명시적인 기본 동작이 있는지 살펴봅니다.
- 초안이 변경될 때마다 최종 작업을 수행하는 변경 관찰자나 모델 세터가 있는지 확인합니다.
비어 있는 로컬 onSubmit 클로저는 문서화된 제출 경계를 대체하지 않습니다. Apple은 전파를 중단하는 용도로 submitScope()를 제공합니다. 자동 완성이 활성 상태가 아닐 때만 Return으로 앱을 제출해야 한다면 추가 상태와 명령 처리를 통해 조건부 키보드 정책을 구현해야 합니다. Apple: onSubmit(of:_:)
Return 키로 항목을 선택하고 처리를 중단해야 할 때 AppKit 사용하기
소유한 NSTextField에는 AppKit의 control(_:textView:doCommandBy:)이 제공됩니다. 이 메서드의 텍스트 뷰 인수는 컨트롤의 필드 편집기입니다. 델리게이트는 바인딩된 명령을 처리한 뒤 true을 반환할 수 있습니다. false를 반환하면 해당 델리게이트에서는 처리되지 않은 상태로 남습니다. 이는 텍스트 필드의 명령을 사용자 지정하는 문서화된 확장 지점입니다. Apple: control(_:textView:doCommandBy:)
사용자 지정 자동 완성 컨트롤러는 이 확장 지점에서 다음 순서를 사용할 수 있습니다.
- 제안 목록이 열려 있는지, 어느 항목이 강조되어 있는지 추적합니다.
- 유효한 강조 항목이 있을 때 Return 명령을 받으면 해당 항목을 초안에 삽입합니다.
- 제안 목록을 닫고 앱의 초안 모델을 동기화합니다.
- 컨트롤러가 명령을 처리했으므로
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 누르기 | 앱이 초안을 적용하지 않음 |
Apply Label 클릭 |
적용된 값이 초안으로 복사되고 카운터가 한 번 증가함 |
| 이후 초안 편집 | 이전에 적용된 값이 계속 표시됨 |
| 초안 지우기 | 제안이 비어 있고 Apply Label이 비활성화됨 |
bl 입력 |
예제의 대소문자 구분 규칙에 따라 일치 항목이 없음 |
기본 제안 목록에서 키보드로 항목을 선택하는 동작도 확인하고, 실제 상위 뷰에 필드를 넣은 뒤에도 검사를 반복하세요. Apple은 macOS 창 이동과 같은 이벤트로 기본 제안 인터페이스가 사라질 수 있다고 설명하므로, 제안 목록이 없어도 초안을 사용할 수 있어야 합니다. Apple: textInputSuggestions(_:)
배포 대상이 macOS 15보다 이전이면 사용 가능 버전 확인을 통해 기본 뷰와 대체 뷰를 제공하거나, 앱 자체 제안 버튼만 사용하세요. 대체 버튼은 레이블을 초안에 바로 할당하고 최종 동작은 Apply Label에 남겨둘 수 있습니다. 이렇게 하면 초안과 적용을 동일하게 분리할 수 있으며, 키보드 탐색과 표시는 컴포넌트에서 처리해야 합니다.
이 레이블 선택기 작업에서 유용한 경계는 관찰 가능합니다. 자동 완성을 선택하면 편집 가능한 초안이 바뀌고, 명시적인 최종 작업만 적용된 레이블을 바꿉니다. 이 경계에는 SwiftUI 기본 제안과 제출 범위를 사용하세요. 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