Files
audi-app/companion-app/native/teilen/ShareViewController.swift
T
Paul Nothaft 2f7669b15b Erweiterung: gerade Anfuehrungszeichen beendeten den Swift-String vorzeitig
Erster Kompilierlauf der Datei ueberhaupt - vorher hing sie in keiner
Sources-Phase und wurde nie uebersetzt. Die deutschen Anfuehrungszeichen in
der Hinweismeldung waren gemischt: oeffnend typografisch (U+201E), schliessend
aber gerade (U+0022). In einem Swift-String beendet das gerade Zeichen die
Zeichenkette, der Rest der Zeile war Unsinn.

Jetzt beidseitig typografisch. In Zeile 97 stehen nur noch die zwei geraden
Anfuehrungszeichen des Strings selbst.
2026-09-02 15:01:36 +02:00

158 lines
6.0 KiB
Swift

//
// 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)
}
}