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>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Entitlements der Haupt-App. Bislang hatte sie keine eigene Datei - erst die
|
||||
App-Gruppe der Share-Erweiterung macht eine noetig. Derselbe Bezeichner wie
|
||||
in native/teilen/DataMetric360Share.entitlements.
|
||||
-->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.datametric360</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Die App-Gruppe ist der einzige gemeinsame Boden zwischen Erweiterung und
|
||||
App - ohne sie kann die Erweiterung nichts uebergeben. Derselbe Bezeichner
|
||||
muss in App.entitlements stehen und im Apple-Entwicklerportal als App Group
|
||||
angelegt sein.
|
||||
-->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.datametric360</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Info.plist der Share-Erweiterung.
|
||||
|
||||
NSExtensionActivationRule entscheidet, wann DataMetric360 im Teilen-Blatt
|
||||
ueberhaupt auftaucht. Bewusst eng: genau EIN PDF, sonst nichts. Die
|
||||
Erweiterung kann nur Tankbelege, und ein Eintrag, der bei jedem Foto und
|
||||
jedem Link mitangeboten wird und dann ablehnt, ist schlechter als keiner.
|
||||
|
||||
NSExtensionPrincipalClass zeigt auf die Swift-Klasse ohne Storyboard -
|
||||
die Erweiterung hat keine eigene Oberflaeche, sie nimmt die Datei entgegen
|
||||
und oeffnet die App. Deshalb steht hier auch kein NSExtensionMainStoryboard.
|
||||
-->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>de</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>DataMetric360</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsAttachmentsWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,157 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user