NFC-Tags fuer Einzelstuecke: beschreiben und lesen

Ein aufgeklebtes QR-Etikett uebersteht keine Waesche. Kleidungsstuecke bekommen
deshalb eingenaehte NFC-Tags. Auf das Tag kommt genau das, was auch im QR steckt
(vorrania://i/<UID>) - damit bleibt das Einlesen inhaltlich unveraendert, nur die
Erfassungsart ist neu. Backend und Web brauchten nichts: die UID vergibt der
Server, und GET /items/by-uid/<UID> gibt es laengst.

Gelesen wird nur bei geoeffneter App. iOS startet Apps beim Antippen im
Hintergrund ausschliesslich ueber https-Universal-Links auf einer beanspruchten
Domain und ignoriert eigene Schemes dabei - eine feste Domain ist fuer einen
selbstgehosteten Dienst aber gerade der falsche Weg (siehe f981f41).

Die /i/-Zerlegung lag dreifach kopiert in CheckInView, LookupScanView und
AssignScanView und steckt jetzt in einzelstueckCode(aus:). Die nackte UID wird
nur fuer NFC akzeptiert: eine achtstellige EAN-8 kann zufaellig genau wie eine
UID aussehen, und die Kamera sieht fremde Barcodes - ein Tag haben wir selbst
beschrieben.

Zwei Altlasten in denselben Dateien mit erledigt:
- project.yml deklarierte die applinks-Domain weiter, obwohl f981f41 sie
  entfernt hat. XcodeGen erzeugt die Entitlements aus diesem Block und schrieb
  sie bei jedem "generate" zurueck; die Domain existiert laengst nicht mehr.
- AssignScanView sendete ItemUpdateRequest ohne priceCents/currency. Da encode
  immer alle Felder schreibt (auch null), hat ein Zuordnen per Scan Kaufpreis
  und Waehrung stillschweigend geloescht.

Ebenfalls raus: QRImage und qrLink(forUid:) in ItemViews - ungenutzt, und
qrLink baute noch das alte https-Format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-12 13:37:24 +02:00
co-authored by Claude Opus 5
parent 9be772557e
commit 09a763a823
11 changed files with 508 additions and 56 deletions
+124
View File
@@ -0,0 +1,124 @@
import SwiftUI
/// Ein NFC-Tag mit dem Link auf ein Einzelstück beschreiben.
///
/// Gedacht für Kleidung: Das Tag wird eingenäht und ersetzt den QR, der die
/// Wäsche nicht überstehen würde. Auf das Tag kommt genau das, was auch im QR
/// steht – `vorrania://i/<UID>`.
struct NFCSchreibenView: View {
let item: Item
@Environment(\.dismiss) private var dismiss
@State private var sperren = false
@State private var laeuft = false
@State private var erfolg = false
@State private var fehler: String?
private var nutzlast: String { einzelstueckLink(fuer: item.uid) }
var body: some View {
Form {
Section {
if let name = item.productName {
LabeledContent("Artikel", value: name)
}
LabeledContent("UID", value: item.uid)
} footer: {
Text("Auf das Tag wird geschrieben: \(nutzlast)")
}
Section {
Toggle("Tag dauerhaft schreibschützen", isOn: $sperren)
} footer: {
if sperren {
Text("Das lässt sich nicht rückgängig machen. Das Tag gehört danach "
+ "für immer zu diesem Einzelstück und kann nicht neu beschrieben werden.")
.foregroundStyle(.orange)
} else {
Text("Ohne Schreibschutz kannst du das Tag später einem anderen "
+ "Einzelstück zuweisen.")
}
}
Section {
Button(laeuft ? "Tag anhalten…" : "Tag beschreiben") {
Task { await schreiben() }
}
.disabled(laeuft)
}
if erfolg {
Section {
Label("Tag beschrieben.", systemImage: "checkmark.circle")
.foregroundStyle(.green)
}
}
if let fehler {
Section {
Text(fehler).foregroundStyle(.red).font(.callout)
}
}
}
.navigationTitle("NFC-Tag")
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } }
}
private func schreiben() async {
laeuft = true
fehler = nil
erfolg = false
defer { laeuft = false }
do {
try await NFCTag.schreiben(nutzlast, sperren: sperren)
erfolg = true
} catch NFCTag.Fehler.abgebrochen {
// Kein Fehler, sondern eine Entscheidung – nichts anzeigen.
} catch {
fehler = error.localizedDescription
}
}
}
/// Knopf, der ein NFC-Tag liest und die gefundene Einzelstück-UID zurückgibt.
///
/// Blendet sich auf Geräten ohne NFC (iPad) selbst aus, damit die umgebenden
/// Ansichten das nicht jedes Mal selbst prüfen müssen.
struct NFCLesenButton: View {
var titel: String = "NFC-Tag lesen"
var onUid: (String) -> Void
var onFehler: (String) -> Void
@State private var laeuft = false
var body: some View {
if NFCTag.verfuegbar {
Button {
Task { await lesen() }
} label: {
Label(laeuft ? "Tag anhalten…" : titel, systemImage: "wave.3.right")
}
.disabled(laeuft)
}
}
private func lesen() async {
laeuft = true
defer { laeuft = false }
do {
let roh = try await NFCTag.lesen()
// Ein selbst beschriebenes Tag darf auch die nackte UID tragen – anders
// als bei der Kamera droht hier keine Verwechslung mit einem Barcode.
guard let uid = einzelstueckCode(aus: roh, nackteUidErlauben: true) else {
onFehler("Auf dem Tag steht kein Einzelstück: \(roh)")
return
}
onUid(uid)
} catch NFCTag.Fehler.abgebrochen {
// Nutzer hat abgebrochen – keine Meldung.
} catch {
onFehler(error.localizedDescription)
}
}
}