import UIKit import Social import UniformTypeIdentifiers /// Mail (and any other text/URL source) Share Extension. Parses /// flight info out of the shared content using the same regex /// patterns as the calendar importer, writes the result to an App /// Group UserDefaults entry under `pendingMailShare`, and dismisses. /// /// The main app reads that entry on next foreground (via /// PendingShareWatcher) and pops the AddFlightView prefilled with /// whatever we parsed. final class ShareViewController: SLComposeServiceViewController { private var parsed: ParsedFlight? private var allText: String = "" struct ParsedFlight { let flightDate: Date let carrierIATA: String? let flightNumber: String? let departureIATA: String? let arrivalIATA: String? } override func viewDidLoad() { super.viewDidLoad() title = "Add to Flights" placeholder = "Optional note" loadSharedItems() } private func loadSharedItems() { guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else { return } let group = DispatchGroup() var accumulated = "" for item in extensionItems { // Mail surfaces both the subject line (as the contentText) // and the body (as attachments). We absorb both. if let content = item.attributedContentText?.string, !content.isEmpty { accumulated += " " + content } for provider in item.attachments ?? [] { if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { group.enter() provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in defer { group.leave() } if let s = item as? String { accumulated += " " + s } } } if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { group.enter() provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, _ in defer { group.leave() } if let u = item as? URL { accumulated += " " + u.absoluteString } } } } } group.notify(queue: .main) { [weak self] in guard let self else { return } self.allText = accumulated self.parsed = Self.parseFlight(from: accumulated) self.validateContent() } } override func isContentValid() -> Bool { return parsed != nil } override func didSelectPost() { guard let parsed else { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) return } // Hand the parsed flight off to the host app via a custom URL // scheme. Share Extensions can't call UIApplication.shared // directly, but we can walk the responder chain to find one // that implements `openURL:` and invoke it. iOS still routes // it through the host app correctly. var comps = URLComponents() comps.scheme = "flights" comps.host = "import" var items: [URLQueryItem] = [ URLQueryItem(name: "date", value: String(parsed.flightDate.timeIntervalSince1970)) ] if let c = parsed.carrierIATA { items.append(.init(name: "carrier", value: c)) } if let f = parsed.flightNumber { items.append(.init(name: "num", value: f)) } if let d = parsed.departureIATA { items.append(.init(name: "dep", value: d)) } if let a = parsed.arrivalIATA { items.append(.init(name: "arr", value: a)) } comps.queryItems = items if let url = comps.url { openURLInHost(url) } extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } /// Walk the responder chain looking for an object that implements /// `openURL:`. UIApplication is one. Invoking it from a share /// extension launches the host app via its registered URL scheme. private func openURLInHost(_ url: URL) { var responder: UIResponder? = self let selector = NSSelectorFromString("openURL:") while responder != nil { if responder!.responds(to: selector) { _ = responder!.perform(selector, with: url) return } responder = responder?.next } } override func configurationItems() -> [Any]! { return [] } // MARK: - Parser private static func parseFlight(from text: String) -> ParsedFlight? { guard let flightMatch = matchFlight(in: text) else { return nil } let route = matchRoute(in: text) let date = matchDate(in: text) ?? Date() return ParsedFlight( flightDate: date, carrierIATA: flightMatch.carrier, flightNumber: flightMatch.number, departureIATA: route?.from, arrivalIATA: route?.to ) } private static func matchFlight(in s: String) -> (carrier: String, number: String)? { let pattern = "([A-Z]{2,3})\\s*([0-9]{1,4})" guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } let nsRange = NSRange(s.startIndex..., in: s) let denylist: Set = ["AM", "PM", "ET", "PT", "CT", "MT", "US", "UK", "TO", "AS"] for m in regex.matches(in: s, range: nsRange) where m.numberOfRanges == 3 { guard let cRange = Range(m.range(at: 1), in: s), let nRange = Range(m.range(at: 2), in: s) else { continue } let carrier = String(s[cRange]) if denylist.contains(carrier) { continue } return (carrier, String(s[nRange])) } return nil } private static func matchRoute(in s: String) -> (from: String, to: String)? { let pattern = "([A-Z]{3})\\s*(?:[-→>]|to)\\s*([A-Z]{3})" guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } let nsRange = NSRange(s.startIndex..., in: s) guard let m = regex.firstMatch(in: s, range: nsRange), m.numberOfRanges == 3, let fRange = Range(m.range(at: 1), in: s), let tRange = Range(m.range(at: 2), in: s) else { return nil } return (String(s[fRange]), String(s[tRange])) } private static func matchDate(in s: String) -> Date? { // ISO-ish: "May 27, 2026" / "27 May 2026" / "2026-05-27" let formatters: [String] = [ "MMMM d, yyyy", "MMM d, yyyy", "d MMMM yyyy", "d MMM yyyy", "yyyy-MM-dd", "MM/dd/yyyy" ] // Try matching against any substring with each formatter. for fmt in formatters { let df = DateFormatter() df.dateFormat = fmt df.locale = Locale(identifier: "en_US_POSIX") // Slide a window through the text; for date formats with // word months we need substrings starting with a month. let words = s.split(whereSeparator: { !$0.isLetter && !$0.isNumber && $0 != "-" && $0 != "/" && $0 != "," }).map(String.init) for i in 0..