Natural Language Text Analysis in SwiftUI

DevTechie Inc
Jul 10, 2023

Natural Language Text Analysis in SwiftUI

iOS’s NaturalLanguage Framework provides built-in API for text analysis to detect lexical class, speech, lemma, scripts and many more.

This API was introduced with the release of iOS 12.

NaturalLanguage framework defines NLTagger class which can be used to find text components. While creating NLTagger instance, we can specify the component we are interested in by specifying an array of NLTagScheme.

NLTagger supports many different languages and scripts and we use it to segment natural language text into paragraph, sentence, or word units and to tag each unit with information like part of speech, lexical class, lemma, script, and language.


Getting Started

For our project setup, we will create a simple view with TextEditor to enter text along with button for text analysis. We will show results in a List view.

struct DevTechieNLTaggerExample: View {
    @State private var inputString = ""
    @State private var result = [String]()
    var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

Lexical Class Detection

We can use lexical class to analyze the text to determine whether the give words are nouns, verbs, adjectives, and other parts of speech in a string.

We will create an instance of NLTagger with tagScheme to be lexicalClass

struct DevTechieNLTaggerExample: View {
    @State private var inputString = ""
    @State private var result = [String]()
let tagger = NLTagger(tagSchemes: [.lexicalClass])
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

We will assign default text for TextEditor and will also assign tagger.string to the value of inputString.

struct DevTechieNLTaggerExample: View {
    @State private var inputString = "DevTechie is the best place to learn iOS Development."
    @State private var result = [String]()
let tagger = NLTagger(tagSchemes: [.lexicalClass])
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    tagger.string = inputString
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

We use enumeratedTags method to iterate over the given range of a string. This method divides up the string with the given NLTokenUnit and NLTagScheme and then calls the block. For example, we use the lexicalClass tag scheme to identify which tokens are parts of speech, types of whitespace, or types of punctuation.

struct DevTechieNLTaggerExample: View {
    @State private var inputString = "DevTechie is the best place to learn iOS Development."
    @State private var result = [String]()
let tagger = NLTagger(tagSchemes: [.lexicalClass])
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    tagger.string = inputString
                    tagger.enumerateTags(in: inputString.startIndex..<inputString.endIndex, unit: .word, scheme: .lexicalClass) { tag, range in
                        result.append("\(inputString[range]) : \(tag!.rawValue)")
                        return true
                    }
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

We can see lots of whitespace being classified and added to our results array. We can remove that by passing NLTagger.Options and ask the tagger to remove whitespaces and punctuations

struct DevTechieNLTaggerExample: View {
    @State private var inputString = "DevTechie is the best place to learn iOS Development."
    @State private var result = [String]()
let tagger = NLTagger(tagSchemes: [.lexicalClass])
    let options: NLTagger.Options = [.omitWhitespace, .omitPunctuation]
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    tagger.string = inputString
                    tagger.enumerateTags(in: inputString.startIndex..<inputString.endIndex, unit: .word, scheme: .lexicalClass, options: options) { tag, range in
                        result.append("\(inputString[range]) : \(tag!.rawValue)")
                        return true
                    }
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

NameType

Let’s take another example string where we will add some names in the input string.

We will use string

Steve Jobs Founded Apple with Steve Woz.

Notice that the NLTagger was able to identify names as Noun. But first name and last name are not combined. We can change that by adding NameType to the tagSchemes parameter while creating NLTagger instance.

let tagger = NLTagger(tagSchemes: [.lexicalClass, .nameType])

Setting just the tag scheme will not do the job, we will have to specify that we want to join names in NLTagger.Options

let options: NLTagger.Options = [.omitWhitespace, .omitPunctuation, .joinNames]

With all the changes, our code should look like this

struct DevTechieNLTaggerExample: View {
    @State private var inputString = "Steve Jobs Founded Apple with Steve Woz."
    @State private var result = [String]()
    let tagger = NLTagger(tagSchemes: [.lexicalClass, .nameType])
    let options: NLTagger.Options = [.omitWhitespace, .omitPunctuation, .joinNames]
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    tagger.string = inputString
                    tagger.enumerateTags(in: inputString.startIndex..<inputString.endIndex, unit: .word, scheme: .lexicalClass, options: options) { tag, range in
                        result.append("\(inputString[range]) : \(tag!.rawValue)")
                        return true
                    }
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

Since combining name type and lexical class is such a common task that NLP team at Apple added option to set both option in a single value called nameTypeOrLexicalClass. Added benefit we get by setting this option is the names are identified as PersonalName instead of noun.

struct DevTechieNLTaggerExample: View {
    @State private var inputString = "Steve Jobs Founded Apple with Steve Woz."
    @State private var result = [String]()
    let tagger = NLTagger(tagSchemes: [.nameTypeOrLexicalClass])
    let options: NLTagger.Options = [.omitWhitespace, .omitPunctuation, .joinNames]
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    tagger.string = inputString
                    tagger.enumerateTags(in: inputString.startIndex..<inputString.endIndex, unit: .word, scheme: .nameTypeOrLexicalClass, options: options) { tag, range in
                        result.append("\(inputString[range]) : \(tag!.rawValue)")
                        return true
                    }
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}

If we use nameType for both Tag Scheme and Scheme for enumeratedTags, we can recognize not only names of people but organizations and places as well. So we will change our input string to include a place name and change Apple to Apple Inc and rerun the project.

import NaturalLanguage
struct DevTechieNLTaggerExample: View {
    @State private var inputString = "Steve Jobs Founded Apple Inc with Steve Woz in Silicon Valley."
    @State private var result = [String]()
    let tagger = NLTagger(tagSchemes: [.nameType])
    let options: NLTagger.Options = [.omitWhitespace, .omitPunctuation, .joinNames]
var body: some View {
        NavigationStack {
            VStack {
                TextEditor(text: $inputString)
                    .overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.orange.gradient, lineWidth: 2))
                Button("Process") {
                    tagger.string = inputString
                    tagger.enumerateTags(in: inputString.startIndex..<inputString.endIndex, unit: .word, scheme: .nameType, options: options) { tag, range in
                        result.append("\(inputString[range]) : \(tag!.rawValue)")
                        return true
                    }
                }
                .buttonStyle(.borderedProminent)
                List(result, id: \.self) { item in
                    Text(item)
                }
            }
            .padding()
            .navigationTitle("DevTechie")
        }
    }
}