b1411625aa
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>
158 lines
6.0 KiB
Swift
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)
|
|
}
|
|
}
|