Created
August 4, 2026 20:09
-
-
Save lanserxt/9b3026ced6be863e9fcbbdc67fd98c97 to your computer and use it in GitHub Desktop.
iOS 26: Data Detection - find entities in String
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // DataDetectorView.swift | |
| // WWDC25Demo | |
| // | |
| // Created by Anton Gubarenko on 04.08.2026. | |
| // | |
| import SwiftUI | |
| import DataDetection | |
| struct DataDetectorView: View { | |
| @State private var model = DataDetectorModel() | |
| var body: some View { | |
| NavigationStack { | |
| List { | |
| Section { | |
| Picker("Example", selection: $model.selectedExampleID) { | |
| ForEach(model.examples) { example in | |
| Text(example.title) | |
| .tag(example.id) | |
| } | |
| } | |
| Button("Detect") { | |
| Task { | |
| await model.detect() | |
| } | |
| } | |
| .disabled(model.isDetecting) | |
| } | |
| if let example = model.selectedExample { | |
| Section("Input") { | |
| Text(example.text) | |
| .textSelection(.enabled) | |
| LabeledContent( | |
| "Expected result", | |
| value: example.expectedResult | |
| ) | |
| } | |
| } | |
| if model.isDetecting { | |
| Section { | |
| ProgressView("Analyzing text") | |
| } | |
| } | |
| if let errorMessage = model.errorMessage { | |
| Section { | |
| ContentUnavailableView( | |
| "Detection Failed", | |
| systemImage: "exclamationmark.triangle", | |
| description: Text(errorMessage) | |
| ) | |
| } | |
| } | |
| if !model.results.isEmpty { | |
| Section("Detected Results") { | |
| ForEach(model.results) { result in | |
| VStack(alignment: .leading, spacing: 8) { | |
| Text(result.type) | |
| .font(.headline) | |
| LabeledContent( | |
| "Result", | |
| value: result.matchedText | |
| ) | |
| LabeledContent( | |
| "Highlight", | |
| value: result.highlightStyle | |
| ) | |
| } | |
| .padding(.vertical, 4) | |
| } | |
| } | |
| } else if model.hasCompletedDetection && !model.isDetecting { | |
| Section { | |
| ContentUnavailableView( | |
| "No Matches", | |
| systemImage: "text.magnifyingglass", | |
| description: Text( | |
| "The detector did not find the requested semantic type in this example." | |
| ) | |
| ) | |
| } | |
| } | |
| } | |
| .navigationTitle("Data Detector") | |
| .task { | |
| await model.detect() | |
| } | |
| .onChange(of: model.selectedExampleID) { | |
| Task { | |
| await model.detect() | |
| } | |
| } | |
| } | |
| } | |
| } | |
| @MainActor | |
| @Observable | |
| final class DataDetectorModel { | |
| struct Example: Identifiable, Hashable { | |
| let id: String | |
| let title: String | |
| let text: String | |
| let types: DataDetector.MatchType | |
| let expectedType: String | |
| let expectedResult: String | |
| let options: DataDetector.Options | |
| init( | |
| id: String, | |
| title: String, | |
| text: String, | |
| types: DataDetector.MatchType, | |
| expectedType: String, | |
| expectedResult: String, | |
| options: DataDetector.Options = .init() | |
| ) { | |
| self.id = id | |
| self.title = title | |
| self.text = text | |
| self.types = types | |
| self.expectedType = expectedType | |
| self.expectedResult = expectedResult | |
| self.options = options | |
| } | |
| } | |
| struct DetectionResult: Identifiable, Hashable { | |
| let id = UUID() | |
| let type: String | |
| let matchedText: String | |
| let highlightStyle: String | |
| } | |
| private(set) var results: [DetectionResult] = [] | |
| private(set) var isDetecting = false | |
| private(set) var hasCompletedDetection = false | |
| private(set) var errorMessage: String? | |
| var selectedExampleID = "email" | |
| let examples: [Example] = { | |
| var calendarOptions = DataDetector.Options() | |
| calendarOptions.documentDate = Date() | |
| calendarOptions.documentTimeZone = .current | |
| calendarOptions.documentLanguageCode = "en" | |
| calendarOptions.documentRegion = "US" | |
| return [ | |
| Example( | |
| id: "email", | |
| title: "Email Address", | |
| text: "Send the build to qa@example.com", | |
| types: [.emailAddress], | |
| expectedType: "emailAddress", | |
| expectedResult: "qa@example.com" | |
| ), | |
| Example( | |
| id: "phone", | |
| title: "Phone Number", | |
| text: "Call me at +1 415 555 1212", | |
| types: [.phoneNumber], | |
| expectedType: "phoneNumber", | |
| expectedResult: "+1 415 555 1212" | |
| ), | |
| Example( | |
| id: "link", | |
| title: "Link", | |
| text: "Read https://developer.apple.com/documentation/datadetection", | |
| types: [.link], | |
| expectedType: "link", | |
| expectedResult: "https://developer.apple.com/documentation/datadetection" | |
| ), | |
| Example( | |
| id: "calendar", | |
| title: "Calendar Event", | |
| text: "Let's meet tomorrow at 10:30 AM", | |
| types: [.calendarEvent], | |
| expectedType: "calendarEvent", | |
| expectedResult: "tomorrow at 10:30 AM", | |
| options: calendarOptions | |
| ), | |
| Example( | |
| id: "address", | |
| title: "Postal Address", | |
| text: "Meet me at 1 Apple Park Way, Cupertino, CA 95014", | |
| types: [.postalAddress], | |
| expectedType: "postalAddress", | |
| expectedResult: "1 Apple Park Way, Cupertino, CA 95014" | |
| ), | |
| Example( | |
| id: "money", | |
| title: "Money Amount", | |
| text: "The subscription costs €9.99 per month", | |
| types: [.moneyAmount], | |
| expectedType: "moneyAmount", | |
| expectedResult: "€9.99" | |
| ), | |
| Example( | |
| id: "measurement", | |
| title: "Measurement", | |
| text: "The package weighs 2.5 kg and is 40 cm wide", | |
| types: [.measurement], | |
| expectedType: "measurement", | |
| expectedResult: "2.5 kg, 40 cm" | |
| ), | |
| Example( | |
| id: "flight", | |
| title: "Flight Number", | |
| text: "My flight is BA 281 from London", | |
| types: [.flightNumber], | |
| expectedType: "flightNumber", | |
| expectedResult: "BA 281" | |
| ), | |
| Example( | |
| id: "tracking", | |
| title: "Tracking Number", | |
| text: "Your UPS tracking number is 1Z999AA10123456784", | |
| types: [.shipmentTrackingNumber], | |
| expectedType: "shipmentTrackingNumber", | |
| expectedResult: "1Z999AA10123456784" | |
| ), | |
| Example( | |
| id: "payment", | |
| title: "Payment Identifier", | |
| text: "Send the payment to anton@bank", | |
| types: [.paymentIdentifier], | |
| expectedType: "paymentIdentifier", | |
| expectedResult: "anton@bank" | |
| ) | |
| ] | |
| }() | |
| var selectedExample: Example? { | |
| examples.first { $0.id == selectedExampleID } | |
| } | |
| func detect() async { | |
| guard let selectedExample else { | |
| return | |
| } | |
| isDetecting = true | |
| hasCompletedDetection = false | |
| errorMessage = nil | |
| results = [] | |
| let detectedResults = await scan( | |
| text: selectedExample.text, | |
| types: selectedExample.types, | |
| options: selectedExample.options | |
| ) | |
| results = detectedResults | |
| hasCompletedDetection = true | |
| isDetecting = false | |
| } | |
| } | |
| private func scan( | |
| text: String, | |
| types: DataDetector.MatchType, | |
| options: DataDetector.Options | |
| ) async -> [DataDetectorModel.DetectionResult] { | |
| var detectedResults: [DataDetectorModel.DetectionResult] = [] | |
| for await match in text.dataDetectorMatches( | |
| types, | |
| options: options | |
| ) { | |
| let matchedText = match.range.map { | |
| String(text[$0]) | |
| } ?? "Unavailable" | |
| detectedResults.append( | |
| DataDetectorModel.DetectionResult( | |
| type: typeName(for: match.details), | |
| matchedText: matchedText, | |
| highlightStyle: highlightDescription( | |
| for: match.preferredHighlightStyle | |
| ) | |
| ) | |
| ) | |
| } | |
| return detectedResults | |
| } | |
| private func typeName( | |
| for details: DataDetector.Match.SemanticDetails | |
| ) -> String { | |
| switch details { | |
| case .emailAddress: | |
| "emailAddress" | |
| case .phoneNumber: | |
| "phoneNumber" | |
| case .link: | |
| "link" | |
| case .postalAddress: | |
| "postalAddress" | |
| case .calendarEvent: | |
| "calendarEvent" | |
| case .moneyAmount: | |
| "moneyAmount" | |
| case .measurement: | |
| "measurement" | |
| case .flightNumber: | |
| "flightNumber" | |
| case .shipmentTrackingNumber: | |
| "shipmentTrackingNumber" | |
| case .paymentIdentifier: | |
| "paymentIdentifier" | |
| @unknown default: | |
| "unknown" | |
| } | |
| } | |
| private func highlightDescription( | |
| for style: DataDetector.Match.HighlightStyle | |
| ) -> String { | |
| switch style { | |
| case .hidden: | |
| "hidden" | |
| case .regular: | |
| "regular" | |
| case .url: | |
| "url" | |
| @unknown default: | |
| "unknown" | |
| } | |
| } | |
| #Preview { | |
| DataDetectorView() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment