Spell Check in SwiftUI Using UITextChecker

DevTechie Inc
Jul 7, 2023

Spell Check in SwiftUI Using UITextChecker

iOS provides a built in spellchecker using UITextChecker. UITextChecker class can be used to check a string for misspelled words. This class has been around for a while, it came into picture with the release of iOS 3.2.

UITextChecker spell-check uses lexicon for specific language to perform spelling correction for supported languages.

In this article, we will use UITextChecker with SwiftUI so let’s get started.

For our app, we will have a TextEditor where user can enter text and bottom part of the screen will show suggestions in a List view

struct DevTechieSpellChecker: View {
    @State private var text = ""
    @State private var gussess: [String]? = []
    
    var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $text)
                    .padding()
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
                
                if let gussess = gussess {
                    List(gussess, id: \.self) { guess in
                        HStack {
                            Text(guess)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                        .onTapGesture {
                           
                        }
                    }
                }
            }
            
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

We will create an instance of UITextChecker . Having one instance per document is fine but we can have single instance to spell-check different parts of the code. We will create just one instance at the struct level.

struct DevTechieSpellChecker: View {
    @State private var text = ""
    @State private var guesses: [String]? = []
    
    let textChecker = UITextChecker()
    
    var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $text)
                    .padding()
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
                
                if let gussess = guesses {
                    List(gussess, id: \.self) { guess in
                        HStack {
                            Text(guess)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                        .onTapGesture {
                           
                        }
                    }
                }
            }
            
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

Next, we will attach an onChange observer for text being typed by the user so UITextChecker can start telling us spelling for misspelled words. We will use guesses array to store the values returned by the UITextChecker.

.onChange(of: text, perform: { newValue in
    let missspelledRange = textChecker.rangeOfMisspelledWord(in: text, range: NSRange(0..<text.utf16.count), startingAt: 0, wrap: false, language: "en_US")
    if missspelledRange.location != NSNotFound {
        guesses = textChecker.guesses(forWordRange: missspelledRange, in: text, language: "en_US")
    }
})

rangeOfMisspelledWord(in:range:startingAt:wrap:language:) initiates a search of a range of a string for a misspelled word. It takes following parameters and returns the range of the first misspelled word encountered or {NSNotFound, 0} if none is found.

stringToCheck The string to check for misspelled words.

range The range of stringToCheck to check for a misspelled word.

startingOffset The offset within range of stringToCheck to begin checking for misspelled words.

wrapFla Set this value to true to continue checking from the beginning of range if no misspelled word is found between startingOffset and the end of range. Specify false to have spell-checking end at the end of range.

language The language of the of words to be checked for correct spelling. This string is a ISO 639–1 language code or a combined ISO 639–1 language code and ISO 3166–1 regional code (for example, fr_FR).

Our code should look like this:

struct DevTechieSpellChecker: View {
    @State private var text = ""
    @State private var guesses: [String]? = []
    
    let textChecker = UITextChecker()
    
    var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $text)
                    .padding()
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
                
                if let gussess = guesses {
                    List(gussess, id: \.self) { guess in
                        HStack {
                            Text(guess)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                        .onTapGesture {
                            
                        }
                    }
                }
            }
            .onChange(of: text, perform: { newValue in
                let missspelledRange = textChecker.rangeOfMisspelledWord(in: text, range: NSRange(0..<text.utf16.count), startingAt: 0, wrap: false, language: "en_US")
                if missspelledRange.location != NSNotFound {
                    guesses = textChecker.guesses(forWordRange: missspelledRange, in: text, language: "en_US")
                }
            })
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

Build and run

It will be great if we can select item from the list of suggestions and autocomplete the word.

We will build a basic autocomplete and to accomplish selection of a suggested word, we will first take the string and divide it in an array of words separated by space.

var words = text.components(separatedBy: " ")

Next, we will remove the last entry from the array of words

words.removeLast()

and will append the guess to the words array.

words.append(guess)

Next, we will replace the text by joining words in the array.

text = words.joined(separator: " ")

Our code should look like this.

struct DevTechieSpellChecker: View {
    @State private var text = ""
    @State private var guesses: [String]? = []
    
    let textChecker = UITextChecker()
    
    var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $text)
                    .padding()
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
                
                if let gussess = guesses {
                    List(gussess, id: \.self) { guess in
                        HStack {
                            Text(guess)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                        .onTapGesture {
                            var words = text.components(separatedBy: " ")
                            words.removeLast()
                            words.append(guess)
                            text = words.joined(separator: " ")
                        }
                    }
                }
            }
            .onChange(of: text, perform: { newValue in
                let missspelledRange = textChecker.rangeOfMisspelledWord(in: text, range: NSRange(0..<text.utf16.count), startingAt: 0, wrap: false, language: "en_US")
                if missspelledRange.location != NSNotFound {
                    guesses = textChecker.guesses(forWordRange: missspelledRange, in: text, language: "en_US")
                }
            })
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

Build and run

UITextChecker includes ways for iOS to learn your vocabulary as well. By using class function called learnWord(_ string:) we can make UITextChecker learn new stuff.

struct DevTechieSpellChecker: View {
    @State private var text = ""
    @State private var gusses: [String]? = []
    
    let textChecker = UITextChecker()
    
    var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $text)
                    .padding()
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.5)))
                
                if let gusses = gussess {
                    List(gussess, id: \.self) { guess in
                        HStack {
                            Text(guess)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                        .onTapGesture {
                            var words = text.components(separatedBy: " ")
                            words.removeLast()
                            words.append(guess)
                            text = words.joined(separator: " ")
                        }
                    }
                }
            }
            .onChange(of: text, perform: { newValue in
                let missspelledRange = textChecker.rangeOfMisspelledWord(in: text, range: NSRange(0..<text.utf16.count), startingAt: 0, wrap: false, language: "en_US")
                if missspelledRange.location != NSNotFound {
                    gussess = textChecker.guesses(forWordRange: missspelledRange, in: text, language: "en_US")
                }
            })
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Learn") {
                        UITextChecker.learnWord(text)
                        print(UITextChecker.hasLearnedWord(text))
                    }
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}