I din't find an easy way to create a SwiftUI TextField which gives the user autocompletion in the sense of filling the TextField with the completion word and highlighting the suffix, so the user can type uninterruptedly, but if there is a suggestion it will be filled in. Here is a simple implementation of this for a SwiftUI TextField:
import SwiftUI
struct SuggestionTextField: View {
let name: String
@Binding var text: String
let suggestions: [String]
@State private var editor: NSText?
@State private var lastSuggested = false
@State private var oldTerm = "" // because we cannot rely on oldText in callback (that changes due to suggestion)
var body: some View {
TextField(name, text: $text)
.onReceive(NotificationCenter.default.publisher(for: NSTextField.textDidBeginEditingNotification)) { obj in
if let textField = obj.object as? NSTextField {
editor = textField.currentEditor()! // Save editor object to set the range on
}
}
.onAppear { oldTerm = text }
.onChange(of: text, onChange)
}
private func onChange(_ oldText: String, _ newText: String) {
guard let editor = editor, // ensure editor is set
!lastSuggested, // ensure the last change was not a suggestion
oldTerm.count < newText.count, // ensure that i can delete characters without a new suggestion
newText.count >= 1 // ensure that at least one character is in text field
else {
if !lastSuggested { oldTerm = newText }
lastSuggested = false
return
}
let term = newText.lowercased()
guard let suggestion = suggestions.first(where: { $0.lowercased().hasPrefix(term) }) else {
return
}
if newText == suggestion { // perfect match, don't do anything here
return
}
text = suggestion
lastSuggested = true
oldTerm = newText
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 1)) {
// Ugly but works, without delay, the range is not selected
editor.selectedRange = NSRange(term.count ... suggestion.count)
}
}
}