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")
}
}
}