Files
audi-app/companion-app/native/teilen/ShareViewController.swift
T
tobias b1411625aa Teilen-Blatt, HIG-Blaetter, geteilte Ansichtseinstellungen (2026.8.31.1)
Share-Erweiterung: ein PDF aus Mail landet ueber Teilen -> DataMetric360 als
Tankbeleg. Alles Native liegt versioniert unter companion-app/native/, weil
ios/ gitignored ist und npx cap add ios es sonst wieder frisst;
scripts/ios-teilen-einrichten.mjs haengt es bei jedem Bau ins Xcode-Projekt
und ist in ios-signieren.sh eingehaengt. Der pbxproj-Teil ist auf keinem Mac
erprobt - er bricht vor dem Schreiben ab, und docs/SHARE_EXTENSION.md
beschreibt dieselben Handgriffe fuer Xcode.

"In den Kalender uebernehmen" ging in der App nie: WKWebView ignoriert das
download-Attribut, der Klick lief ins Leere. Auf dem Geraet jetzt
@capacitor/filesystem plus @capacitor/share.

useTheme/useBoden legten je Aufrufer eigenen Zustand an - fuenf Kopien. Der
Schalter aenderte nur seine eigene, data-theme in App.tsx blieb stehen, die
Tag/Nacht-Umschaltung wirkte erst nach einem Neustart. Jetzt ein Speicher je
Einstellung ueber useSyncExternalStore. Zwei Regressionstests, gegen den
alten Stand als fehlschlagend nachgewiesen.

Alle Popups nach Apple HIG: neue Bausteine Sheet und ActionSheet, blickdicht
und mit abgedunkeltem Schleier, in beiden Codebasen. Popup portalierte nach
document.body - ausserhalb von [data-theme] - und war deshalb im Tagmodus
fast unsichtbar; Portal zielt jetzt auf .ads-root.

ImagePlaceholder merkte sich "fehlgeschlagen" ohne die Adresse: nach einem
leeren Galerieplatz zeigten auch die mit Foto nur noch den Platzhalter.
Vier Regressionstests, ebenfalls gegen den alten Stand geprueft.

Bildadressen tragen jetzt einen Cache-Brecher - HA liefert /local/ mit 31
Tagen Cache-Vorgabe aus, im Panel eingefuegte Radfotos erschienen in der App
deshalb nicht.

uebersichtsbild: Panel speichert den Namen, die App verglich gegen den
Dateinamen - der Vergleich traf nie zu. Kanonisch ist der Name.

Weiter: Navigationspfeil statt gleichschenkligem Dreieck auf der Streckenlinie
(die Kerbe unterscheidet Kopf und Ende), CI-Symbol tour-s als Fahrten-Icon,
Datumsfelder zeigen ihren Wert ohne erstes Antippen, feste Beispielnamen im
Setup, Kopf-Gegengewicht fuer mittige Titel, Standort-Blatt mit festem
Fussabstand, NSLocationWhenInUseUsageDescription, watchPosition fuer die
Live-Ortung, Art-Pille wieder als Knopf, Zwischenablage fuer Tankbelege.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 14:24:52 +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)
}
}