// // ShareViewController.swift // DataMetric360Share // // Nimmt ein PDF aus dem iOS-Teilen-Blatt entgegen (typischerweise ein // Tankbeleg aus Mail) und reicht es an die App weiter. // // WIE DIE ÜBERGABE FUNKTIONIERT — UND WARUM SO // -------------------------------------------- // Eine Share-Erweiterung ist ein eigener Prozess mit eigenem Dateibereich. // Sie kann der App nichts direkt geben; der einzige gemeinsame Boden ist // eine App-Gruppe. Der Weg hier: // // 1. Das PDF wird gelesen und Base64-kodiert. // 2. Es landet zusammen mit seinem Namen in den geteilten // UserDefaults der Gruppe `group.app.datametric360`. // 3. Die Erweiterung öffnet die App über ihr eigenes URL-Schema. // 4. Die App liest den Eintrag beim Start bzw. beim Zurückkehren aus, // löscht ihn und lädt den Beleg hoch (daten/geteilterBeleg.ts). // // Base64 in UserDefaults statt einer Datei im Gruppen-Container: die App // liest UserDefaults ohnehin schon über @capacitor/preferences, das genau // diese Gruppe kennt. Eine Datei im Container bräuchte dagegen eigenen // nativen Code auf der App-Seite — ein zusätzliches Plugin für einen Weg, // den es sonst nicht gäbe. // // Der Preis ist eine Größengrenze: UserDefaults ist für Einstellungen // gedacht, nicht für Anhänge. GRENZE_BYTES hält das bei 4 MB. Ein // Tankbeleg liegt bei einigen Dutzend Kilobyte, ein gescanntes Mehrseiten- // PDF kann darüber liegen — dann sagt die Erweiterung das und der Weg über // „Datei auswählen" in der App bleibt. // // DIESE DATEI IST VERSIONIERT, ios/ NICHT. // `npx cap add ios` erzeugt den nativen Ordner neu und würde alles darin // verlieren. scripts/ios-teilen-einrichten.mjs kopiert diese Datei bei // jedem Bau wieder hinein und hängt das Ziel ins Xcode-Projekt. // import UIKit import Social import UniformTypeIdentifiers class ShareViewController: UIViewController { /// Muss zur App-Gruppe in beiden Entitlements-Dateien passen. private let gruppe = "group.app.datametric360" /// Schlüssel, unter dem die App den Beleg abholt (geteilterBeleg.ts). private let schluessel = "dm360_geteilter_beleg" /// 4 MB. UserDefaults ist kein Dateispeicher — darüber wird abgelehnt, /// statt das System mit einem Anhang zu belasten, für den es nicht /// gedacht ist. private let grenzeBytes = 4 * 1024 * 1024 override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear pdfUebernehmen() } private func pdfUebernehmen() { guard let eintrag = (extensionContext?.inputItems as? [NSExtensionItem])?.first, let anhaenge = eintrag.attachments else { return abschliessen() } let pdfTyp = UTType.pdf.identifier guard let anhang = anhaenge.first(where: { $0.hasItemConformingToTypeIdentifier(pdfTyp) }) else { return melden("Kein PDF dabei.") } anhang.loadItem(forTypeIdentifier: pdfTyp, options: nil) { [weak self] wert, _ in guard let self else { return } // Je nach Quelle kommt eine URL, ein Data-Objekt oder nichts // Brauchbares an — Mail liefert eine URL auf eine temporäre // Kopie, andere Apps reichen die Bytes direkt. var daten: Data? var name = "beleg.pdf" if let url = wert as? URL { daten = try? Data(contentsOf: url) name = url.lastPathComponent } else if let roh = wert as? Data { daten = roh } guard let daten else { return self.melden("Der Beleg ließ sich nicht lesen.") } guard daten.count <= self.grenzeBytes else { return self.melden( "Der Beleg ist zu groß für diesen Weg. In der App über „Beleg hochladen“ → „Datei auswählen“.") } self.ablegen(daten: daten, name: name) } } private func ablegen(daten: Data, name: String) { guard let ablage = UserDefaults(suiteName: gruppe) else { return melden("Die App-Gruppe ist nicht eingerichtet.") } // Form wie in geteilterBeleg.ts erwartet. @capacitor/preferences legt // Werte als String ab, deshalb JSON und kein Dictionary. let inhalt: [String: Any] = [ "name": name, "base64": daten.base64EncodedString(), "zeit": Int(Date().timeIntervalSince1970 * 1000), ] guard let json = try? JSONSerialization.data(withJSONObject: inhalt), let text = String(data: json, encoding: .utf8) else { return melden("Der Beleg ließ sich nicht übergeben.") } ablage.set(text, forKey: schluessel) DispatchQueue.main.async { [weak self] in self?.appOeffnen() self?.abschliessen() } } /// Öffnet die Host-App. Eine Erweiterung darf `UIApplication.shared` nicht /// benutzen, deshalb der Umweg über die Responder-Kette — der einzige von /// Apple vorgesehene Weg aus einer Erweiterung heraus. private func appOeffnen() { guard let ziel = URL(string: "datametric360://beleg") else { return } var antwort: UIResponder? = self while let r = antwort { if let app = r as? UIApplication { app.open(ziel, options: [:], completionHandler: nil) return } antwort = r.next } } private func melden(_ text: String) { DispatchQueue.main.async { [weak self] in let hinweis = UIAlertController(title: "DataMetric360", message: text, preferredStyle: .alert) hinweis.addAction(UIAlertAction(title: "Verstanden", style: .default) { _ in self?.abschliessen() }) self?.present(hinweis, animated: true) } } private func abschliessen() { extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } }