Visual overview
Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.
- Choose language task
- Configure tokenizer or tagger
- Process text units
- Interpret linguistic results
- Boundary 1Text input
- Boundary 2Language analysis
- Boundary 3Structured annotations
Connected responsibilities, not a required class hierarchy or an execution trace.
Overview
The NaturalLanguage framework provides on-device natural language processing for tokenization, language identification, named entity recognition, part-of-speech tagging, sentiment analysis, lemmatization, and text embeddings. All processing runs locally with no network dependency. Every code example below compiles and follows production best practices.
1. Tokenization -- NLTokenizer
Split text into words, sentences, or paragraphs with locale-aware boundary detection.
import NaturalLanguage
func tokenize(_ text: String, unit: NLTokenUnit) -> [String] {
let tokenizer = NLTokenizer(unit: unit)
tokenizer.string = text
var tokens: [String] = []
tokenizer.enumerateTokens(in: text.startIndex..<text.endIndex) { range, _ in
tokens.append(String(text[range]))
return true // continue enumeration
}
return tokens
}
// Usage
let sentence = "The quick brown fox jumps over the lazy dog. It was a sunny day."
let words = tokenize(sentence, unit: .word)
// ["The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog", "It", "was", "a", "sunny", "day"]
let sentences = tokenize(sentence, unit: .sentence)
// ["The quick brown fox jumps over the lazy dog. ", "It was a sunny day."]
let paragraphs = tokenize("First paragraph.\n\nSecond paragraph.", unit: .paragraph)
// ["First paragraph.\n\n", "Second paragraph."]
Tokenizing with Language Hint
import NaturalLanguage
func tokenizeWithLanguage(_ text: String, language: NLLanguage) -> [String] {
let tokenizer = NLTokenizer(unit: .word)
tokenizer.string = text
tokenizer.setLanguage(language)
var tokens: [String] = []
tokenizer.enumerateTokens(in: text.startIndex..<text.endIndex) { range, attributes in
let token = String(text[range])
tokens.append(token)
return true
}
return tokens
}
// Japanese text tokenization
let japaneseTokens = tokenizeWithLanguage("東京は日本の首都です", language: .japanese)
// Properly segments into Japanese word boundaries
// Chinese text tokenization
let chineseTokens = tokenizeWithLanguage("北京是中国的首都", language: .simplifiedChinese)
2. Part-of-Speech Tagging -- NLTagger
Identify the grammatical role of each word: noun, verb, adjective, etc.
import NaturalLanguage
struct TaggedWord {
let word: String
let tag: NLTag?
let tagName: String
}
func tagPartsOfSpeech(_ text: String) -> [TaggedWord] {
let tagger = NLTagger(tagSchemes: [.lexicalClass])
tagger.string = text
var results: [TaggedWord] = []
tagger.enumerateTags(
in: text.startIndex..<text.endIndex,
unit: .word,
scheme: .lexicalClass,
options: [.omitWhitespace, .omitPunctuation]
) { tag, range in
let word = String(text[range])
let tagName: String = switch tag {
case .noun: "Noun"
case .verb: "Verb"
case .adjective: "Adjective"
case .adverb: "Adverb"
case .pronoun: "Pronoun"
case .determiner: "Determiner"
case .particle: "Particle"
case .preposition: "Preposition"
case .conjunction: "Conjunction"
case .interjection: "Interjection"
case .number: "Number"
default: tag?.rawValue ?? "Unknown"
}
results.append(TaggedWord(word: word, tag: tag, tagName: tagName))
return true
}
return results
}
// Usage
let tagged = tagPartsOfSpeech("The quick brown fox jumps over the lazy dog")
for item in tagged {
print("\(item.word): \(item.tagName)")
}
// The: Determiner
// quick: Adjective
// brown: Adjective
// fox: Noun
// jumps: Verb
// over: Preposition
// the: Determiner
// lazy: Adjective
// dog: Noun
3. Named Entity Recognition
Identify people, places, organizations, and other named entities.
import NaturalLanguage
struct NamedEntity {
let text: String
let type: String
}
func extractNamedEntities(_ text: String) -> [NamedEntity] {
let tagger = NLTagger(tagSchemes: [.nameType])
tagger.string = text
var entities: [NamedEntity] = []
tagger.enumerateTags(
in: text.startIndex..<text.endIndex,
unit: .word,
scheme: .nameType,
options: [.omitWhitespace, .omitPunctuation, .joinNames]
) { tag, range in
guard let tag else { return true }
let entityText = String(text[range])
let entityType: String = switch tag {
case .personalName: "Person"
case .placeName: "Place"
case .organizationName: "Organization"
default: tag.rawValue
}
entities.append(NamedEntity(text: entityText, type: entityType))
return true
}
return entities
}
// Usage
let entities = extractNamedEntities("Tim Cook announced the new iPhone at Apple Park in Cupertino.")
// [("Tim Cook", "Person"), ("iPhone", "Organization"), ("Apple Park", "Organization"), ("Cupertino", "Place")]
4. Lemmatization
Reduce words to their base (dictionary) form.
import NaturalLanguage
func lemmatize(_ text: String) -> [(word: String, lemma: String)] {
let tagger = NLTagger(tagSchemes: [.lemma])
tagger.string = text
var results: [(word: String, lemma: String)] = []
tagger.enumerateTags(
in: text.startIndex..<text.endIndex,
unit: .word,
scheme: .lemma,
options: [.omitWhitespace, .omitPunctuation]
) { tag, range in
let word = String(text[range])
let lemma = tag?.rawValue ?? word
results.append((word: word, lemma: lemma))
return true
}
return results
}
// Usage
let lemmas = lemmatize("The dogs were running quickly through the forests")
// [("dogs", "dog"), ("were", "be"), ("running", "run"), ("quickly", "quickly"), ("forests", "forest")]
5. Language Identification -- NLLanguageRecognizer
Detect the language of a text string or rank probable languages.
import NaturalLanguage
func identifyLanguage(_ text: String) -> NLLanguage? {
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
return recognizer.dominantLanguage
}
func rankLanguages(_ text: String, maxResults: Int = 5) -> [(NLLanguage, Double)] {
let recognizer = NLLanguageRecognizer()
recognizer.processString(text)
let hypotheses = recognizer.languageHypotheses(withMaximum: maxResults)
return hypotheses
.sorted { $0.value > $1.value }
.map { ($0.key, $0.value) }
}
// Constrain to expected languages for better accuracy
func identifyLanguageConstrained(_ text: String, candidates: [NLLanguage]) -> NLLanguage? {
let recognizer = NLLanguageRecognizer()
recognizer.languageConstraints = candidates
recognizer.processString(text)
return recognizer.dominantLanguage
}
// Usage
let language = identifyLanguage("Bonjour, comment allez-vous?")
// .french
let ranked = rankLanguages("Das ist ein Test")
// [(.german, 0.98), (.dutch, 0.01), ...]
let constrained = identifyLanguageConstrained(
"Ciao, come stai?",
candidates: [.italian, .spanish, .french]
)
// .italian
6. Sentiment Analysis
Determine the emotional tone of text using the built-in sentiment tagger.
import NaturalLanguage
/// Returns a sentiment score between -1.0 (negative) and 1.0 (positive)
func analyzeSentiment(_ text: String) -> Double {
let tagger = NLTagger(tagSchemes: [.sentimentScore])
tagger.string = text
let (sentimentTag, _) = tagger.tag(
at: text.startIndex,
unit: .paragraph,
scheme: .sentimentScore
)
return Double(sentimentTag?.rawValue ?? "0") ?? 0.0
}
enum Sentiment: String {
case positive, negative, neutral
}
func classifySentiment(_ text: String) -> Sentiment {
let score = analyzeSentiment(text)
if score > 0.1 { return .positive }
if score < -0.1 { return .negative }
return .neutral
}
// Usage
let score1 = analyzeSentiment("I absolutely love this product! It's amazing!")
// ~0.8 (positive)
let score2 = analyzeSentiment("This is terrible. Worst experience ever.")
// ~-0.7 (negative)
let score3 = analyzeSentiment("The meeting is at 3pm in the conference room.")
// ~0.0 (neutral)
let sentiment = classifySentiment("Great job on the presentation!")
// .positive
Sentence-Level Sentiment Analysis
import NaturalLanguage
struct SentenceSentiment {
let sentence: String
let score: Double
let label: Sentiment
}
func analyzeSentimentBySentence(_ text: String) -> [SentenceSentiment] {
// First, split into sentences
let sentenceTokenizer = NLTokenizer(unit: .sentence)
sentenceTokenizer.string = text
var results: [SentenceSentiment] = []
sentenceTokenizer.enumerateTokens(in: text.startIndex..<text.endIndex) { range, _ in
let sentence = String(text[range]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !sentence.isEmpty else { return true }
let score = analyzeSentiment(sentence)
let label: Sentiment
if score > 0.1 { label = .positive }
else if score < -0.1 { label = .negative }
else { label = .neutral }
results.append(SentenceSentiment(sentence: sentence, score: score, label: label))
return true
}
return results
}
7. Text Embeddings -- NLEmbedding
Compute vector representations of words and sentences for semantic similarity.
Word Embeddings
import NaturalLanguage
func wordSimilarity(_ word1: String, _ word2: String, language: NLLanguage = .english) -> Double? {
guard let embedding = NLEmbedding.wordEmbedding(for: language) else { return nil }
// Distance is between 0 (identical) and 2 (opposite)
let distance = embedding.distance(between: word1, and: word2)
// Convert to similarity (1.0 = identical, 0.0 = unrelated)
return 1.0 - (distance / 2.0)
}
func findNearestWords(to word: String, maxResults: Int = 10, language: NLLanguage = .english) -> [(String, Double)] {
guard let embedding = NLEmbedding.wordEmbedding(for: language) else { return [] }
var results: [(String, Double)] = []
embedding.enumerateNeighbors(for: word, maximumCount: maxResults) { neighbor, distance in
let similarity = 1.0 - (distance / 2.0)
results.append((neighbor, similarity))
return true
}
return results
}
func wordVector(_ word: String, language: NLLanguage = .english) -> [Double]? {
guard let embedding = NLEmbedding.wordEmbedding(for: language) else { return nil }
return embedding.vector(for: word)
}
// Usage
let similarity = wordSimilarity("king", "queen")
// ~0.85 (very similar)
let neighbors = findNearestWords(to: "swift", maxResults: 5)
// [("fast", 0.82), ("quick", 0.78), ("rapid", 0.75), ...]
Sentence Embeddings
import NaturalLanguage
@available(iOS 15.0, *)
func sentenceSimilarity(_ sentence1: String, _ sentence2: String, language: NLLanguage = .english) -> Double? {
guard let embedding = NLEmbedding.sentenceEmbedding(for: language) else { return nil }
let distance = embedding.distance(between: sentence1, and: sentence2)
return 1.0 - (distance / 2.0)
}
@available(iOS 15.0, *)
func findSimilarSentences(to query: String, in candidates: [String], language: NLLanguage = .english) -> [(String, Double)] {
guard let embedding = NLEmbedding.sentenceEmbedding(for: language) else { return [] }
return candidates.compactMap { candidate in
let distance = embedding.distance(between: query, and: candidate)
let similarity = 1.0 - (distance / 2.0)
return (candidate, similarity)
}
.sorted { $0.1 > $1.1 }
}
// Usage
let sim = sentenceSimilarity(
"How is the weather today?",
"What's the forecast for today?"
)
// ~0.85 (semantically similar)
let results = findSimilarSentences(
to: "I need help with my account",
in: [
"How do I reset my password?",
"Where is the nearest restaurant?",
"I want to change my profile settings",
"What time does the store close?"
]
)
// Ranked by semantic similarity to the query
8. Custom NLModel with Create ML
Train a custom text classifier and use it with NLTagger.
import NaturalLanguage
import CoreML
// Loading and using a custom NLModel (trained with Create ML)
func loadCustomModel() throws -> NLModel {
let modelURL = Bundle.main.url(forResource: "CustomTextClassifier", withExtension: "mlmodelc")!
return try NLModel(contentsOf: modelURL)
}
// Standalone prediction
func classifyText(_ text: String, model: NLModel) -> String? {
return model.predictedLabel(for: text)
}
// Prediction with confidence scores
func classifyTextWithConfidence(_ text: String, model: NLModel) -> [(String, Double)] {
let hypotheses = model.predictedLabelHypotheses(for: text, maximumCount: 5)
return hypotheses
.sorted { $0.value > $1.value }
.map { ($0.key, $0.value) }
}
// Using a custom model with NLTagger for per-token classification
func tagWithCustomModel(_ text: String, model: NLModel) -> [(String, String?)] {
let tagger = NLTagger(tagSchemes: [.nameType])
tagger.string = text
tagger.setModels([model], forTagScheme: .nameType)
var results: [(String, String?)] = []
tagger.enumerateTags(
in: text.startIndex..<text.endIndex,
unit: .word,
scheme: .nameType,
options: [.omitWhitespace, .omitPunctuation]
) { tag, range in
let word = String(text[range])
results.append((word, tag?.rawValue))
return true
}
return results
}
9. Complete Text Analysis Pipeline
A full SwiftUI view combining multiple NaturalLanguage features.
import SwiftUI
import NaturalLanguage
@Observable
final class TextAnalysisViewModel {
var inputText = ""
var detectedLanguage = ""
var sentimentScore = 0.0
var sentimentLabel = ""
var wordCount = 0
var sentenceCount = 0
var entities: [NamedEntity] = []
var posTagged: [TaggedWord] = []
func analyze() {
guard !inputText.isEmpty else { return }
// Language detection
let recognizer = NLLanguageRecognizer()
recognizer.processString(inputText)
detectedLanguage = recognizer.dominantLanguage?.rawValue ?? "Unknown"
// Sentiment
let sentimentTagger = NLTagger(tagSchemes: [.sentimentScore])
sentimentTagger.string = inputText
let (tag, _) = sentimentTagger.tag(at: inputText.startIndex, unit: .paragraph, scheme: .sentimentScore)
sentimentScore = Double(tag?.rawValue ?? "0") ?? 0.0
if sentimentScore > 0.1 { sentimentLabel = "Positive" }
else if sentimentScore < -0.1 { sentimentLabel = "Negative" }
else { sentimentLabel = "Neutral" }
// Tokenization counts
let wordTokenizer = NLTokenizer(unit: .word)
wordTokenizer.string = inputText
wordCount = 0
wordTokenizer.enumerateTokens(in: inputText.startIndex..<inputText.endIndex) { _, _ in
wordCount += 1
return true
}
let sentenceTokenizer = NLTokenizer(unit: .sentence)
sentenceTokenizer.string = inputText
sentenceCount = 0
sentenceTokenizer.enumerateTokens(in: inputText.startIndex..<inputText.endIndex) { _, _ in
sentenceCount += 1
return true
}
// Named entities
entities = extractNamedEntities(inputText)
// POS tagging (first 20 words)
posTagged = Array(tagPartsOfSpeech(inputText).prefix(20))
}
}
struct TextAnalysisView: View {
@State private var viewModel = TextAnalysisViewModel()
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
TextEditor(text: $viewModel.inputText)
.frame(minHeight: 120)
.padding(8)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12))
Button("Analyze") {
viewModel.analyze()
}
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
if !viewModel.detectedLanguage.isEmpty {
GroupBox("Overview") {
LabeledContent("Language", value: viewModel.detectedLanguage)
LabeledContent("Words", value: "\(viewModel.wordCount)")
LabeledContent("Sentences", value: "\(viewModel.sentenceCount)")
}
GroupBox("Sentiment") {
LabeledContent("Score", value: String(format: "%.2f", viewModel.sentimentScore))
LabeledContent("Label", value: viewModel.sentimentLabel)
}
if !viewModel.entities.isEmpty {
GroupBox("Named Entities") {
ForEach(viewModel.entities, id: \.text) { entity in
LabeledContent(entity.text, value: entity.type)
}
}
}
if !viewModel.posTagged.isEmpty {
GroupBox("Parts of Speech") {
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
], spacing: 8) {
ForEach(viewModel.posTagged, id: \.word) { item in
VStack(spacing: 4) {
Text(item.word)
.font(.body.bold())
Text(item.tagName)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(8)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
}
}
}
}
}
}
.padding()
}
.navigationTitle("Text Analysis")
}
}
}
Quick Reference
| Class | Purpose |
|---|---|
NLTokenizer |
Split text into words, sentences, or paragraphs |
NLTagger |
Tag tokens with POS, NER, lemma, sentiment |
NLLanguageRecognizer |
Detect the language of a string |
NLEmbedding |
Word and sentence vector embeddings |
NLModel |
Load and use custom Create ML text models |
| Tag Scheme | Tags Produced |
|---|---|
.lexicalClass |
Noun, Verb, Adjective, Adverb, Pronoun, Determiner, etc. |
.nameType |
PersonalName, PlaceName, OrganizationName |
.lemma |
Base/dictionary form of each word |
.sentimentScore |
Floating-point score from -1.0 to 1.0 |
.language |
Per-token language identification |
| Embedding Type | Available From | Dimensions |
|---|---|---|
| Word embedding | iOS 13+ | ~128-300 dimensions |
| Sentence embedding | iOS 15+ | ~512 dimensions |