iOS 26: Data Detector
Find semantic objects in string
Imagine a typical note. Transcribed or generated. With e-mails, dates and locations. This is where a client-side detector is a good choice. Previously, there were either custom detector and libs or one nice Objective-C class. Called NSDataDetector and it still works. It can find links, dates, phone numbers, addresses, and a few other useful things inside natural-language text.
But every time I use it, I feel quite far from modern Swift.
We create a detector with NSTextCheckingTypes, pass an NSRange, receive NSTextCheckingResult, inspect resultType, and then open the correct optional property. It is not a bad API. It is simply an API from a very different Apple platform era.
iOS 26 finally gives us a new DataDetector.
And, of course, there is a catch: it requires an iOS 26 minimum deployment target. If your app still supports older systems, NSDataDetector is not going anywhere yet. But for new apps (or code that can be isolated behind availability checks) the new API feels much closer to the Swift we write today.
From NSDataDetector to dataDetectorMatches
The first thing: we do not create a detector object.
DataDetector is exposed through a StringProtocol extension:
import DataDetection
let text = "Contact me at anton@example.com"
for await match in text.dataDetectorMatches([.emailAddress]) {
// anton@example.com
print(match.details)
}The result is an AsyncSequence.
That is already differs from NSDataDetector. We iterate asynchronously, receive a strongly typed DataDetector.Match, and inspect its semantic details. All new strict-type features.
Apple also warns that this work can be resource-intensive and should not block the main thread or another critical task.
One Match, Three Useful Pieces
Each match gives us three things I care about:
for await match in text.dataDetectorMatches([.all]) {
let range = match.range
let details = match.details
let highlightStyle = match.preferredHighlightStyle
}rangepoints back into the original Swift string.detailscontains the semantic result.preferredHighlightStyletells us how Apple suggests presenting that match.
The range is a native Range<String.Index>?, not NSRange.
Let’s Detect Everything
Instead of creating one disconnected snippet per type, let’s use one helper and keep adding inputs to it.
func printMatches(
in text: String,
types: DataDetector.MatchType = .all
) async {
print("Input:", text)
for await match in text.dataDetectorMatches(types) {
let matchedText = match.range.map { String(text[$0]) } ?? "Unknown"
print("Matched text:", matchedText)
print("Details:", match.details)
}
}For demo code, printing match.details is useful because every result carries its own semantic value.
In app code, we normally switch over the enum.
Email Address
Let’s start with the easiest one.
let text = "Send the build to qa@example.com"
for await match in text.dataDetectorMatches([.emailAddress]) {
if case let .emailAddress(email) = match.details {
// qa@example.com
print(email.emailAddress)
}
}No NSTextCheckingResult, no resultType, no optional result.url pretending an email address is just another link.
The semantic type gives us an email address. Exactly as expected.
Phone Number
Phone numbers follow the same pattern:
let text = "Call me at +1 415 555 1212"
for await match in text.dataDetectorMatches([.phoneNumber]) {
if case let .phoneNumber(phone) = match.details {
// +1 415 555 1212
print(phone.phoneNumber)
}
}And here is an important detail: use the semantic value, not only the visible substring.
Phone numbers may contain extensions or formatting that is more complex than simply copying the matched characters. Apple explicitly warns against inferring all semantic information from the range alone.
Links
Links are not limited to a string starting with https://.
let text = "Read more at <https://developer.apple.com/documentation/datadetection>"
for await match in text.dataDetectorMatches([.link]) {
if case let .link(link) = match.details {
// <https://developer.apple.com/documentation/datadetection>
print(String(describing: link))
}
}I use String(describing:) in the small examples because the associated semantic structures can evolve. And it’s great for debugging )
Calendar Events
This one is more interesting because natural-language dates depend on context.
var options = DataDetector.Options()
options.documentDate = Date()
options.documentTimeZone = .current
options.documentLanguageCode = "en"
options.documentRegion = "US"
let text = "Let's meet tomorrow at 10:30 AM"
for await match in text.dataDetectorMatches(
[.calendarEvent],
options: options
) {
if case let .calendarEvent(event) = match.details {
// tomorrow at 10:30 AM
print(String(describing: event))
}
}“Tomorrow” is meaningless without a document date.
“10:30” can also mean different things depending on language, region, and time zone.
That is why DataDetector.Options exists. It lets us provide hints such as:
document date;
time zone;
language code;
region.
They are hints, not strict parser settings. Still, giving the framework real context is much better than hoping it guesses correctly.
Postal Address
Postal addresses are one of the first cases where the detector becomes more than a glorified regular expression.
let text = "Meet me at 1 Apple Park Way, Cupertino, CA 95014"
for await match in text.dataDetectorMatches([.postalAddress]) {
if case let .postalAddress(address) = match.details {
// 1 Apple Park Way, Cupertino, CA 95014
print(String(describing: address))
}
}Do not expect identical behavior for every address format in every country.
Natural-language detection is intentionally conservative. If the framework is uncertain, it may skip a value instead of returning a risky match.
That is useful for UI suggestions.
It is not useful as form validation.
Money Amount
Money is new territory compared with the old “links and phone numbers” examples most of us wrote for NSDataDetector.
let text = "The subscription costs €9.99 per month"
for await match in text.dataDetectorMatches([.moneyAmount]) {
if case let .moneyAmount(amount) = match.details {
// €9.99
print(String(describing: amount))
}
}This can be useful for expense apps, invoice previews, chat messages, or imported notes.
But again: detection is not financial parsing.
For machine-readable prices, use a formatter or your API model. Data Detector is for natural-language text.
Measurements
Measurements work the same way:
let text = "The package weighs 2.5 kg and is 40 cm wide"
for await match in text.dataDetectorMatches([.measurement]) {
if case let .measurement(measurement) = match.details {
// 2.5 kg
// 40 cm
print(String(describing: measurement))
}
}One input can naturally produce several matches.
That is why the AsyncSequence model fits nicely. One source.
Flight Number
Now something more domain-specific:
let text = "My flight is BA 281 from London"
for await match in text.dataDetectorMatches([.flightNumber]) {
if case let .flightNumber(flight) = match.details {
// BA 281
print(String(describing: flight))
}
}Imagine a REGEXP to find this… Gladly it’s working.
Shipment Tracking Number
Shipment tracking is similar:
let text = "Your UPS tracking number is 1Z999AA10123456784"
for await match in text.dataDetectorMatches([.shipmentTrackingNumber]) {
if case let .shipmentTrackingNumber(tracking) = match.details {
// 1Z999AA10123456784
print(String(describing: tracking))
}
}This is useful for shopping apps, mail clients, support chats, and delivery notifications.
Payment Identifiers
The detector can also look for payment identifiers such as UPI identifiers:
let text = "Send the payment to anton@bank"
for await match in text.dataDetectorMatches([.paymentIdentifier]) {
if case let .paymentIdentifier(identifier) = match.details {
// anton@bank
print(String(describing: identifier))
}
}Even, this is detected fine.
One Switch for All Types
Once you understand the pattern, the complete scanner is quite compact:
func describe(_ match: DataDetector.Match) -> String {
switch match.details {
case let .emailAddress(value):
"Email: \(value.emailAddress)"
case let .phoneNumber(value):
"Phone: \(value.phoneNumber)"
case let .link(value):
"Link: \(String(describing: value))"
case let .postalAddress(value):
"Address: \(String(describing: value))"
case let .calendarEvent(value):
"Calendar event: \(String(describing: value))"
case let .moneyAmount(value):
"Money: \(String(describing: value))"
case let .measurement(value):
"Measurement: \(String(describing: value))"
case let .flightNumber(value):
"Flight: \(String(describing: value))"
case let .shipmentTrackingNumber(value):
"Tracking: \(String(describing: value))"
case let .paymentIdentifier(value):
"Payment identifier: \(String(describing: value))"
@unknown default:
"Unknown match"
}
}And that is the main improvement probably. It is replacing the old checking-result programming model with an asynchronous, strongly typed Swift API.
Highlighting Matches
DataDetector.Match also provides a preferred highlight style:
for await match in text.dataDetectorMatches(.all) {
switch match.preferredHighlightStyle {
case .hidden:
break
case .url:
print("Present like a regular link")
case .regular:
print("Use a subtle highlight")
}
}The available styles are:
.hidden.url.regular
This is a suggestion, of course.
Still, I like that the semantic result and the presentation hint come together. It makes building Mail-like or Messages-like text interactions easier.
Do Not Use It for Validation
This deserves its own section.
Data Detector scans natural-language text.
It does not validate a form field.
let text = "My email is probably anton@example"The detector may skip an uncertain value. That is expected.
For validation:
create a
URL;use
FormatStyleorParseStrategy;use a domain-specific parser;
validate on the backend where appropriate.
Detection asks:
Does this sentence appear to contain an email address?
Validation asks:
Is this input acceptable for my product rules?
Those are different questions.
Why Not Just Keep NSDataDetector
You can, definitely and for apps supporting iOS 25 or earlier, you probably have to.
But compare the programming models.
The old flow:
let detector = try NSDataDetector(
types: NSTextCheckingResult.CheckingType.link.rawValue
)
let range = NSRange(text.startIndex..., in: text)
let matches = detector.matches(in: text, range: range)
for match in matches {
if match.resultType == .link {
print(match.url)
}
}The new flow:
for await match in text.dataDetectorMatches([.link]) {
if case let .link(link) = match.details {
print(link)
}
}The old API is synchronous, range-heavy, and centered around one polymorphic Objective-C result class.
The new API uses:
Swift ranges
AsyncSequencesemantic enum cases
associated typed values
optional context hints
presentation hints
That is enough reason for me to prefer it in iOS 26-only code.
Supporting Older Systems
For a real application, hide both implementations behind one interface:
protocol TextDetecting {
func detect(in text: String) async -> [DetectedItem]
}Then use:
DataDetectoron iOS 26 and laterNSDataDetectoron earlier systems
This keeps Objective-C-era range conversion out of the rest of your feature code.
And one day, when iOS 26 becomes your minimum target, deleting the fallback will be quite satisfying.
Where It Fits
I see this API being useful in:
messaging apps
note-taking apps
mail clients
support chats
delivery and shopping apps
expense and receipt tools
document previews
custom text editors
imported OCR results
I would not run .all over every large text block only because the API exists.
Choose the types your UI can actually use.
Detecting a flight number is only valuable when tapping it leads somewhere.
Limitations
The new API requires iOS 26
It detects natural-language entities; it does not validate user input
Results can vary by language, region, context, and ambiguity
Some semantic structures may evolve while the SDK is new
Large inputs can be expensive
Not every detected value needs to become an interactive UI element
Apps supporting older systems still need an
NSDataDetectorfallback
Complete Project
View the complete DataDetector SwiftUI sample on GitHub Gist
Happy coding!
References
Apple Documentation: DataDetection
Apple Documentation: DataDetector
Apple Documentation: dataDetectorMatches(_:options:)
Apple Documentation: DataDetector.Options
Apple Documentation: DataDetector.Match
Apple Documentation: DataDetector.Match.HighlightStyle
Apple Documentation: NSDataDetector

