Skip to content

Instantly share code, notes, and snippets.

@jschiefner
Created October 26, 2023 21:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jschiefner/67d30ad9aa5698bfec7cab3eb22e2eee to your computer and use it in GitHub Desktop.
Save jschiefner/67d30ad9aa5698bfec7cab3eb22e2eee to your computer and use it in GitHub Desktop.
Autocomplete Suggestion TextField in SwiftUI

SwiftUI TextField with Suggestions

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)
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment