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 (siehef981f41). 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, obwohlf981f41sie 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:
co-authored by
Claude Opus 5
parent
9be772557e
commit
09a763a823
+17
-1
@@ -63,6 +63,21 @@ Es gibt zwei Wege direkt in den Scan-Bildschirm:
|
|||||||
|
|
||||||
Beide Wege öffnen die App **direkt in der Kamera**.
|
Beide Wege öffnen die App **direkt in der Kamera**.
|
||||||
|
|
||||||
|
## NFC-Tags für Kleidung
|
||||||
|
Ein aufgeklebtes QR-Etikett übersteht keine Wäsche. Für Kleidungsstücke gibt es
|
||||||
|
deshalb eingenähte NFC-Tags: Einzelstück öffnen ▸ **NFC-Tag beschreiben** ▸ Tag
|
||||||
|
an die Oberseite des iPhones halten. Auf das Tag kommt derselbe Link, der auch im
|
||||||
|
QR steckt (`vorrania://i/<UID>`).
|
||||||
|
|
||||||
|
Gelesen wird es unter **Nachschlagen** oder **Ort zuordnen** über
|
||||||
|
*NFC-Tag lesen*. Das Antippen bei gesperrtem Handy startet die App bewusst
|
||||||
|
nicht: iOS macht das nur über Universal Links auf einer festen Domain, und die
|
||||||
|
gibt es bei einem selbst gehosteten Dienst nicht.
|
||||||
|
|
||||||
|
> Zum Bauen muss für die App-ID im Apple-Developer-Portal die Capability
|
||||||
|
> *Near Field Communication Tag Reading* aktiv sein, sonst schlägt das Signieren
|
||||||
|
> fehl. NFC gibt es nur auf dem iPhone – auf dem iPad sind die Knöpfe ausgeblendet.
|
||||||
|
|
||||||
## Aufbau der Quellen
|
## Aufbau der Quellen
|
||||||
| Datei | Inhalt |
|
| Datei | Inhalt |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -79,7 +94,8 @@ Beide Wege öffnen die App **direkt in der Kamera**.
|
|||||||
| `ProductDetailView.swift` | Produkt bearbeiten, Verwaltungsart ändern, Chargen korrigieren |
|
| `ProductDetailView.swift` | Produkt bearbeiten, Verwaltungsart ändern, Chargen korrigieren |
|
||||||
| `ObjectStockView.swift` | Gegenstände: Menge je Lagerort (hinzufügen/umlagern/entfernen) |
|
| `ObjectStockView.swift` | Gegenstände: Menge je Lagerort (hinzufügen/umlagern/entfernen) |
|
||||||
| `ItemViews.swift` / `ItemListView.swift` | Einzelstücke anlegen, bearbeiten und auflisten |
|
| `ItemViews.swift` / `ItemListView.swift` | Einzelstücke anlegen, bearbeiten und auflisten |
|
||||||
| `AssignScanView.swift` | Einzelstück (Scan oder Liste) einem Lagerort-QR zuordnen |
|
| `AssignScanView.swift` | Einzelstück (Scan, NFC oder Liste) einem Lagerort-QR zuordnen |
|
||||||
|
| `NFCTag.swift` / `NFCViews.swift` | NFC-Tags lesen und beschreiben, Einzelstück-Code zerlegen |
|
||||||
| `ListViews.swift` | Produktlisten, Einkaufsliste, „bald ablaufend", Mindestbestände |
|
| `ListViews.swift` | Produktlisten, Einkaufsliste, „bald ablaufend", Mindestbestände |
|
||||||
| `DashboardView.swift` / `DashboardCards.swift` / `ChartCards.swift` | Übersicht mit Karten und Diagrammen |
|
| `DashboardView.swift` / `DashboardCards.swift` / `ChartCards.swift` | Übersicht mit Karten und Diagrammen |
|
||||||
| `NotificationScheduler.swift` / `NotificationSettings*.swift` | Push-Benachrichtigungen bei Ablauf |
|
| `NotificationScheduler.swift` / `NotificationSettings*.swift` | Push-Benachrichtigungen bei Ablauf |
|
||||||
|
|||||||
@@ -38,7 +38,19 @@ struct AssignScanView: View {
|
|||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Text("Schritt 1: Einzelstück-QR scannen – oder ohne Code:")
|
Text("Schritt 1: Einzelstück-QR scannen – oder ohne Code:")
|
||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
// Für Stücke ohne eigenen QR (z.B. Kleidung): aus der Liste wählen.
|
// Kleidung trägt keinen QR, aber ein eingenähtes NFC-Tag.
|
||||||
|
NFCLesenButton(titel: "Einzelstück-Tag lesen") { uid in
|
||||||
|
Task {
|
||||||
|
let gefunden = await waehle(uid: uid)
|
||||||
|
if !gefunden {
|
||||||
|
error = "Kein Einzelstück mit der UID \(uid)."
|
||||||
|
status = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} onFehler: { meldung in
|
||||||
|
error = meldung; status = nil
|
||||||
|
}
|
||||||
|
// Für Stücke ohne Code jeder Art: aus der Liste wählen.
|
||||||
Button { showPicker = true } label: {
|
Button { showPicker = true } label: {
|
||||||
Label("Einzelstück aus Liste wählen", systemImage: "list.bullet")
|
Label("Einzelstück aus Liste wählen", systemImage: "list.bullet")
|
||||||
}
|
}
|
||||||
@@ -78,9 +90,14 @@ struct AssignScanView: View {
|
|||||||
error = "Erst ein Einzelstück scannen."; status = nil; return
|
error = "Erst ein Einzelstück scannen."; status = nil; return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
|
// Alle Felder mitsenden: ItemUpdateRequest schreibt auch null
|
||||||
|
// mit, damit sich Felder leeren lassen. Kaufpreis und Waehrung
|
||||||
|
// fehlten hier – ein Zuordnen per Scan hat sie stillschweigend
|
||||||
|
// geloescht.
|
||||||
_ = try await APIClient.shared.updateItem(id: it.id, ItemUpdateRequest(
|
_ = try await APIClient.shared.updateItem(id: it.id, ItemUpdateRequest(
|
||||||
locationId: locCode, shopId: it.shopId,
|
locationId: locCode, shopId: it.shopId,
|
||||||
acquiredOn: it.acquiredOn, warrantyUntil: it.warrantyUntil, note: it.note))
|
acquiredOn: it.acquiredOn, warrantyUntil: it.warrantyUntil, note: it.note,
|
||||||
|
priceCents: it.priceCents, currency: it.currency))
|
||||||
status = "\(it.uid) zugeordnet. Nächstes Stück scannen."
|
status = "\(it.uid) zugeordnet. Nächstes Stück scannen."
|
||||||
error = nil
|
error = nil
|
||||||
pending = nil
|
pending = nil
|
||||||
@@ -89,16 +106,23 @@ struct AssignScanView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Einzelstück-QR (…/i/<UID>)?
|
// Einzelstück-QR (…/i/<UID>)?
|
||||||
if let r = code.range(of: "/i/") {
|
if let uid = einzelstueckCode(aus: code), await waehle(uid: uid) {
|
||||||
let uid = String(code[r.upperBound...])
|
return
|
||||||
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
|
|
||||||
if !uid.isEmpty, let it = try? await APIClient.shared.itemByUid(uid: uid) {
|
|
||||||
pending = it; status = nil; error = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
error = "Unbekannter Code – bitte einen Einzelstück- oder Lagerort-QR scannen."
|
error = "Unbekannter Code – bitte einen Einzelstück- oder Lagerort-QR scannen."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Einzelstück als „wartet auf Lagerort" vormerken – aus Scan, NFC oder Liste.
|
||||||
|
/// Gibt zurueck, ob es gefunden wurde; der Aufrufer entscheidet ueber die
|
||||||
|
/// Fehlermeldung, weil ein Scan noch andere Deutungen zulaesst als ein Tag.
|
||||||
|
@discardableResult
|
||||||
|
private func waehle(uid: String) async -> Bool {
|
||||||
|
guard let it = try? await APIClient.shared.itemByUid(uid: uid) else { return false }
|
||||||
|
pending = it
|
||||||
|
status = nil
|
||||||
|
error = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Einzelstück aus einer durchsuchbaren Liste wählen – für Stücke ohne eigenen QR.
|
/// Einzelstück aus einer durchsuchbaren Liste wählen – für Stücke ohne eigenen QR.
|
||||||
|
|||||||
@@ -311,15 +311,11 @@ struct CheckInView: View {
|
|||||||
unknownCode = nil
|
unknownCode = nil
|
||||||
do {
|
do {
|
||||||
// Einzelstück-QR (…/i/<UID>) → direkt das Stück öffnen statt Barcode-Suche.
|
// Einzelstück-QR (…/i/<UID>) → direkt das Stück öffnen statt Barcode-Suche.
|
||||||
if let r = code.range(of: "/i/") {
|
if let uid = einzelstueckCode(aus: code),
|
||||||
let uid = String(code[r.upperBound...])
|
let item = try? await APIClient.shared.itemByUid(uid: uid) {
|
||||||
.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
|
scannedItem = item
|
||||||
.uppercased()
|
scanShown = false
|
||||||
if !uid.isEmpty, let item = try? await APIClient.shared.itemByUid(uid: uid) {
|
return
|
||||||
scannedItem = item
|
|
||||||
scanShown = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let result = try await APIClient.shared.lookup(barcode: code)
|
let result = try await APIClient.shared.lookup(barcode: code)
|
||||||
suggestedGroupId = result.groupId
|
suggestedGroupId = result.groupId
|
||||||
|
|||||||
@@ -65,6 +65,10 @@
|
|||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Die Kamera wird zum Scannen von Barcodes beim Ein- und Auslagern verwendet.</string>
|
<string>Die Kamera wird zum Scannen von Barcodes beim Ein- und Auslagern verwendet.</string>
|
||||||
|
|
||||||
|
<!-- NFC für die eingenähten Tags an Einzelstücken -->
|
||||||
|
<key>NFCReaderUsageDescription</key>
|
||||||
|
<string>NFC wird zum Lesen und Beschreiben der Tags an Einzelstücken verwendet.</string>
|
||||||
|
|
||||||
<!-- Lokaler Server ohne HTTPS (Heimnetz) -->
|
<!-- Lokaler Server ohne HTTPS (Heimnetz) -->
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
+11
-23
@@ -1,31 +1,8 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CoreImage
|
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
import QuickLook
|
import QuickLook
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
/// QR-Code als Bild (CoreImage). Inhalt ist der Link auf das Einzelstück, damit
|
|
||||||
/// ein Scan – auch mit der Systemkamera – die App/Weboberfläche öffnet.
|
|
||||||
enum QRImage {
|
|
||||||
static func make(_ text: String, scale: CGFloat = 8) -> UIImage? {
|
|
||||||
guard let data = text.data(using: .utf8),
|
|
||||||
let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil }
|
|
||||||
filter.setValue(data, forKey: "inputMessage")
|
|
||||||
filter.setValue("M", forKey: "inputCorrectionLevel")
|
|
||||||
guard let ci = filter.outputImage?
|
|
||||||
.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) else { return nil }
|
|
||||||
let context = CIContext()
|
|
||||||
guard let cg = context.createCGImage(ci, from: ci.extent) else { return nil }
|
|
||||||
return UIImage(cgImage: cg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func qrLink(forUid uid: String) -> String {
|
|
||||||
let base = Session.shared.baseURL?.absoluteString
|
|
||||||
.trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
|
|
||||||
return base.isEmpty ? uid : "\(base)/i/\(uid)"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func dateToString(_ d: Date) -> String {
|
private func dateToString(_ d: Date) -> String {
|
||||||
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f.string(from: d)
|
let f = DateFormatter(); f.dateFormat = "yyyy-MM-dd"; return f.string(from: d)
|
||||||
}
|
}
|
||||||
@@ -217,6 +194,7 @@ struct ItemEditView: View {
|
|||||||
@State private var pickerItem: PhotosPickerItem?
|
@State private var pickerItem: PhotosPickerItem?
|
||||||
@State private var previewURL: URL?
|
@State private var previewURL: URL?
|
||||||
@State private var locScanShown = false
|
@State private var locScanShown = false
|
||||||
|
@State private var nfcShown = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -234,6 +212,13 @@ struct ItemEditView: View {
|
|||||||
LabeledContent("Marke", value: brand)
|
LabeledContent("Marke", value: brand)
|
||||||
}
|
}
|
||||||
LabeledContent("UID", value: item.uid)
|
LabeledContent("UID", value: item.uid)
|
||||||
|
// Für Kleidung: ein eingenähtes NFC-Tag hält, was ein aufgeklebter
|
||||||
|
// QR nicht übersteht. Auf dem iPad ausgeblendet – dort gibt es kein NFC.
|
||||||
|
if NFCTag.verfuegbar {
|
||||||
|
Button { nfcShown = true } label: {
|
||||||
|
Label("NFC-Tag beschreiben", systemImage: "wave.3.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -337,6 +322,9 @@ struct ItemEditView: View {
|
|||||||
LocationScannerView(locations: locations) { loc in locationId = loc.id }
|
LocationScannerView(locations: locations) { loc in locationId = loc.id }
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $nfcShown) {
|
||||||
|
NavigationStack { NFCSchreibenView(item: item) }
|
||||||
|
}
|
||||||
.sheet(isPresented: $showRemove) {
|
.sheet(isPresented: $showRemove) {
|
||||||
NavigationStack { removeSheet }
|
NavigationStack { removeSheet }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,21 @@ struct LookupScanView: View {
|
|||||||
.font(.caption).foregroundStyle(.secondary)
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
// Eingenähte Tags an Kleidung lassen sich nicht scannen.
|
||||||
|
NFCLesenButton { uid in
|
||||||
|
Task {
|
||||||
|
paused = true
|
||||||
|
if let found = try? await APIClient.shared.itemByUid(uid: uid) {
|
||||||
|
item = found
|
||||||
|
} else {
|
||||||
|
error = "Kein Einzelstück mit der UID \(uid)."
|
||||||
|
paused = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} onFehler: { meldung in
|
||||||
|
error = meldung
|
||||||
|
}
|
||||||
|
|
||||||
if let error {
|
if let error {
|
||||||
Text(error).font(.callout).foregroundStyle(.red)
|
Text(error).font(.callout).foregroundStyle(.red)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -58,13 +73,10 @@ struct LookupScanView: View {
|
|||||||
paused = true
|
paused = true
|
||||||
error = nil
|
error = nil
|
||||||
// Einzelstück-QR (…/i/<UID>)?
|
// Einzelstück-QR (…/i/<UID>)?
|
||||||
if let r = code.range(of: "/i/") {
|
if let uid = einzelstueckCode(aus: code),
|
||||||
let uid = String(code[r.upperBound...])
|
let found = try? await APIClient.shared.itemByUid(uid: uid) {
|
||||||
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
|
item = found
|
||||||
if !uid.isEmpty, let found = try? await APIClient.shared.itemByUid(uid: uid) {
|
return
|
||||||
item = found
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Lagerort-QR (…/l/<Code>)? → alles zeigen, was dort (inkl. Unterorte) liegt.
|
// Lagerort-QR (…/l/<Code>)? → alles zeigen, was dort (inkl. Unterorte) liegt.
|
||||||
if let r = code.range(of: "/l/") {
|
if let r = code.range(of: "/l/") {
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import AudioToolbox
|
||||||
|
import CoreNFC
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Einzelstück-Codes
|
||||||
|
|
||||||
|
/// Zeichenvorrat der vom Server vergebenen Einzelstück-UID.
|
||||||
|
/// Spiegelt `_ALPHABET` in backend/app/services/items.py – ohne 0/O/1/I/L.
|
||||||
|
private let uidAlphabet = Set("ABCDEFGHJKMNPQRSTUVWXYZ23456789")
|
||||||
|
private let uidLaenge = 8
|
||||||
|
|
||||||
|
/// Was auf ein NFC-Tag geschrieben und in einen QR kodiert wird.
|
||||||
|
/// Wortgleich mit `itemDeepLink` in web/src/qr.jsx – bewusst ein eigenes Scheme
|
||||||
|
/// statt einer https-Adresse, damit der Code keinen Server verrät und auf jeder
|
||||||
|
/// selbst gehosteten Instanz funktioniert.
|
||||||
|
func einzelstueckLink(fuer uid: String) -> String { "vorrania://i/\(uid)" }
|
||||||
|
|
||||||
|
/// Zieht die Einzelstück-UID aus einem gescannten oder gelesenen Code.
|
||||||
|
///
|
||||||
|
/// Erkennt `vorrania://i/<UID>` und die frühere Form `https://server/i/<UID>`,
|
||||||
|
/// beide über das Teilstück `/i/`.
|
||||||
|
///
|
||||||
|
/// `nackteUidErlauben` ist nur für NFC gedacht. Eine achtstellige EAN-8 kann
|
||||||
|
/// zufällig genau wie eine UID aussehen (z.B. „96385274" – acht Zeichen, alle im
|
||||||
|
/// Alphabet), und die Kamera sieht beliebige fremde Barcodes. Sie würde dann als
|
||||||
|
/// Einzelstück durchgehen, statt im Katalog nachgeschlagen zu werden. Ein
|
||||||
|
/// NFC-Tag dagegen haben wir selbst beschrieben.
|
||||||
|
func einzelstueckCode(aus roh: String, nackteUidErlauben: Bool = false) -> String? {
|
||||||
|
let text = roh.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
if let r = text.range(of: "/i/") {
|
||||||
|
let uid = String(text[r.upperBound...])
|
||||||
|
.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
|
||||||
|
.uppercased()
|
||||||
|
return uid.isEmpty ? nil : uid
|
||||||
|
}
|
||||||
|
|
||||||
|
guard nackteUidErlauben else { return nil }
|
||||||
|
let uid = text.uppercased()
|
||||||
|
guard uid.count == uidLaenge, uid.allSatisfy({ uidAlphabet.contains($0) }) else { return nil }
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NFC
|
||||||
|
|
||||||
|
/// Liest und beschreibt NDEF-Tags – gedacht für die in Kleidung eingenähten
|
||||||
|
/// Tags, wo ein aufgeklebter QR die Wäsche nicht überstehen würde.
|
||||||
|
///
|
||||||
|
/// Core NFC meldet sich über ein Delegate auf einer eigenen Warteschlange; nach
|
||||||
|
/// aussen sieht das hier wie ein normales `await` aus, damit die Views genauso
|
||||||
|
/// aussehen wie beim Kamera-Scan.
|
||||||
|
///
|
||||||
|
/// Gelesen wird nur mit geöffneter App. iOS startet Apps beim Antippen im
|
||||||
|
/// Hintergrund ausschliesslich über https-Universal-Links auf einer beanspruchten
|
||||||
|
/// Domain – eigene Schemes ignoriert es dabei stillschweigend. Eine feste Domain
|
||||||
|
/// wäre für einen selbst gehosteten Dienst aber gerade der falsche Weg (siehe
|
||||||
|
/// Kommentar an `Router.handle(url:)`).
|
||||||
|
final class NFCTag: NSObject {
|
||||||
|
|
||||||
|
/// Ob das Gerät NFC lesen kann. Auf dem iPad immer `false` – die App läuft
|
||||||
|
/// dort ebenfalls (`TARGETED_DEVICE_FAMILY "1,2"`), deshalb muss jede
|
||||||
|
/// NFC-Bedienung daran hängen.
|
||||||
|
static var verfuegbar: Bool { NFCNDEFReaderSession.readingAvailable }
|
||||||
|
|
||||||
|
enum Fehler: LocalizedError {
|
||||||
|
case nichtVerfuegbar
|
||||||
|
case abgebrochen
|
||||||
|
case leer
|
||||||
|
case schreibgeschuetzt
|
||||||
|
case zuKlein(benoetigt: Int, platz: Int)
|
||||||
|
case nichtBeschreibbar
|
||||||
|
case system(String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .nichtVerfuegbar:
|
||||||
|
return "Dieses Gerät kann keine NFC-Tags lesen."
|
||||||
|
case .abgebrochen:
|
||||||
|
return "Abgebrochen."
|
||||||
|
case .leer:
|
||||||
|
return "Auf dem Tag steht nichts."
|
||||||
|
case .schreibgeschuetzt:
|
||||||
|
return "Dieses Tag ist schreibgeschützt und lässt sich nicht mehr ändern."
|
||||||
|
case let .zuKlein(benoetigt, platz):
|
||||||
|
return "Zu wenig Platz auf dem Tag (\(benoetigt) Byte nötig, \(platz) frei)."
|
||||||
|
case .nichtBeschreibbar:
|
||||||
|
return "Dieses Tag lässt sich nicht beschreiben."
|
||||||
|
case let .system(text):
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Auftrag {
|
||||||
|
case lesen
|
||||||
|
case schreiben(nutzlast: String, sperren: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let queue = DispatchQueue(label: "com.scarriffle.vorrania.nfc")
|
||||||
|
private var session: NFCNDEFReaderSession?
|
||||||
|
private var auftrag: Auftrag = .lesen
|
||||||
|
private var weiter: CheckedContinuation<String, Error>?
|
||||||
|
/// Die Sitzung hält sich selbst am Leben, solange sie läuft – sonst gibt der
|
||||||
|
/// Aufrufer die Instanz frei und das Delegate meldet sich nie.
|
||||||
|
private var festhalten: NFCTag?
|
||||||
|
|
||||||
|
// MARK: Öffentlich
|
||||||
|
|
||||||
|
/// Liest ein Tag und gibt seinen Inhalt roh zurück.
|
||||||
|
static func lesen() async throws -> String {
|
||||||
|
try await NFCTag().starten(.lesen, hinweis: "Halte das Tag an die Oberseite des iPhones.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Beschreibt ein Tag. `sperren` macht es **unwiderruflich** schreibgeschützt.
|
||||||
|
static func schreiben(_ nutzlast: String, sperren: Bool = false) async throws {
|
||||||
|
_ = try await NFCTag().starten(
|
||||||
|
.schreiben(nutzlast: nutzlast, sperren: sperren),
|
||||||
|
hinweis: "Halte das Tag an die Oberseite des iPhones."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Ablauf
|
||||||
|
|
||||||
|
private func starten(_ auftrag: Auftrag, hinweis: String) async throws -> String {
|
||||||
|
guard Self.verfuegbar else { throw Fehler.nichtVerfuegbar }
|
||||||
|
self.auftrag = auftrag
|
||||||
|
return try await withCheckedThrowingContinuation { fortsetzung in
|
||||||
|
weiter = fortsetzung
|
||||||
|
festhalten = self
|
||||||
|
let s = NFCNDEFReaderSession(delegate: self, queue: queue, invalidateAfterFirstRead: false)
|
||||||
|
s.alertMessage = hinweis
|
||||||
|
session = s
|
||||||
|
s.begin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Beendet den Vorgang genau einmal. Nach dem Invalidieren meldet sich das
|
||||||
|
/// Delegate noch ein weiteres Mal – ohne diese Sperre liefe `resume` zweimal
|
||||||
|
/// und die App stürzte ab.
|
||||||
|
private func fertig(_ ergebnis: Result<String, Error>, meldung: String? = nil) {
|
||||||
|
guard let fortsetzung = weiter else { return }
|
||||||
|
weiter = nil
|
||||||
|
if let meldung {
|
||||||
|
session?.alertMessage = meldung
|
||||||
|
session?.invalidate()
|
||||||
|
} else if case .failure = ergebnis {
|
||||||
|
session?.invalidate()
|
||||||
|
}
|
||||||
|
session = nil
|
||||||
|
fortsetzung.resume(with: ergebnis)
|
||||||
|
festhalten = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func lesen(_ tag: NFCNDEFTag) {
|
||||||
|
tag.readNDEF { nachricht, fehler in
|
||||||
|
if let fehler {
|
||||||
|
self.fertig(.failure(Fehler.system(fehler.localizedDescription)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let text = nachricht?.records.compactMap(Self.text(aus:)).first, !text.isEmpty else {
|
||||||
|
self.fertig(.failure(Fehler.leer))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AudioServicesPlaySystemSound(1057)
|
||||||
|
self.fertig(.success(text), meldung: "Gelesen.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func schreiben(_ tag: NFCNDEFTag, nutzlast: String, sperren: Bool) {
|
||||||
|
guard let record = NFCNDEFPayload.wellKnownTypeURIPayload(string: nutzlast) else {
|
||||||
|
fertig(.failure(Fehler.system("Der Inhalt lässt sich nicht in ein NDEF-Format bringen.")))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let nachricht = NFCNDEFMessage(records: [record])
|
||||||
|
|
||||||
|
tag.queryNDEFStatus { status, kapazitaet, fehler in
|
||||||
|
if let fehler {
|
||||||
|
self.fertig(.failure(Fehler.system(fehler.localizedDescription)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch status {
|
||||||
|
case .notSupported:
|
||||||
|
self.fertig(.failure(Fehler.nichtBeschreibbar))
|
||||||
|
return
|
||||||
|
case .readOnly:
|
||||||
|
self.fertig(.failure(Fehler.schreibgeschuetzt))
|
||||||
|
return
|
||||||
|
case .readWrite:
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
self.fertig(.failure(Fehler.nichtBeschreibbar))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard nachricht.length <= kapazitaet else {
|
||||||
|
self.fertig(.failure(Fehler.zuKlein(benoetigt: nachricht.length, platz: kapazitaet)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.writeNDEF(nachricht) { fehler in
|
||||||
|
if let fehler {
|
||||||
|
self.fertig(.failure(Fehler.system(fehler.localizedDescription)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard sperren else {
|
||||||
|
AudioServicesPlaySystemSound(1057)
|
||||||
|
self.fertig(.success(nutzlast), meldung: "Tag beschrieben.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tag.writeLock { fehler in
|
||||||
|
if let fehler {
|
||||||
|
// Geschrieben ist geschrieben – nur das Sperren ging schief.
|
||||||
|
// Das gehört gesagt, statt den ganzen Vorgang als
|
||||||
|
// gescheitert auszugeben.
|
||||||
|
self.fertig(.failure(Fehler.system(
|
||||||
|
"Tag beschrieben, aber das Sperren ist fehlgeschlagen: "
|
||||||
|
+ fehler.localizedDescription)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AudioServicesPlaySystemSound(1057)
|
||||||
|
self.fertig(.success(nutzlast), meldung: "Tag beschrieben und gesperrt.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holt den lesbaren Inhalt aus einem Record – URI oder Text.
|
||||||
|
private static func text(aus record: NFCNDEFPayload) -> String? {
|
||||||
|
if let url = record.wellKnownTypeURIPayload() { return url.absoluteString }
|
||||||
|
// Liefert ein Tupel aus Optionals, nicht ein optionales Tupel.
|
||||||
|
if let text = record.wellKnownTypeTextPayload().0 { return text }
|
||||||
|
return String(data: record.payload, encoding: .utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NFCNDEFReaderSessionDelegate
|
||||||
|
|
||||||
|
extension NFCTag: NFCNDEFReaderSessionDelegate {
|
||||||
|
|
||||||
|
/// Wird nicht aufgerufen, solange `didDetect tags:` vorhanden ist – Core NFC
|
||||||
|
/// nimmt dann den Weg über die Tags. Das Protokoll verlangt die Methode
|
||||||
|
/// trotzdem.
|
||||||
|
func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {}
|
||||||
|
|
||||||
|
func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) {
|
||||||
|
guard tags.count == 1, let tag = tags.first else {
|
||||||
|
session.alertMessage = "Mehrere Tags erkannt. Bitte nur eines anhalten."
|
||||||
|
session.restartPolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.connect(to: tag) { fehler in
|
||||||
|
if let fehler {
|
||||||
|
self.fertig(.failure(Fehler.system(fehler.localizedDescription)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch self.auftrag {
|
||||||
|
case .lesen:
|
||||||
|
self.lesen(tag)
|
||||||
|
case let .schreiben(nutzlast, sperren):
|
||||||
|
self.schreiben(tag, nutzlast: nutzlast, sperren: sperren)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
|
||||||
|
// Hat der Vorgang schon ein Ergebnis geliefert, ist das hier nur noch das
|
||||||
|
// Aufräumen danach.
|
||||||
|
guard weiter != nil else { return }
|
||||||
|
let abbruch = (error as? NFCReaderError)?.code
|
||||||
|
== .readerSessionInvalidationErrorUserCanceled
|
||||||
|
self.session = nil
|
||||||
|
fertig(.failure(abbruch ? Fehler.abgebrochen : Fehler.system(error.localizedDescription)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.developer.associated-domains</key>
|
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:vorrania-ch.scarriffle.com</string>
|
<string>NDEF</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0D8629839E465D80EC58E74B /* ProductViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 369B9841E43E727ACA2E2A2A /* ProductViews.swift */; };
|
0D8629839E465D80EC58E74B /* ProductViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 369B9841E43E727ACA2E2A2A /* ProductViews.swift */; };
|
||||||
0E0AE6CC6FBA82B190BBC36F /* CheckInFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E56353CB483EFB1CCD09D8 /* CheckInFormView.swift */; };
|
0E0AE6CC6FBA82B190BBC36F /* CheckInFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99E56353CB483EFB1CCD09D8 /* CheckInFormView.swift */; };
|
||||||
|
10F43F9BA3CB785DEBBC625F /* NFCViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B6163E03197495FAA1F298 /* NFCViews.swift */; };
|
||||||
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */; };
|
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */; };
|
||||||
25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */; };
|
25FC3913EFFA37D013328FDE /* CategoryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */; };
|
||||||
34787DA16A1AB035BF129BB7 /* GroupViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED41226394E7F31F37EC3B8 /* GroupViews.swift */; };
|
34787DA16A1AB035BF129BB7 /* GroupViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED41226394E7F31F37EC3B8 /* GroupViews.swift */; };
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
AAF27320D2A67C994C29C26F /* ProductDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F574168AA0F849D46C384EE /* ProductDetailView.swift */; };
|
AAF27320D2A67C994C29C26F /* ProductDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F574168AA0F849D46C384EE /* ProductDetailView.swift */; };
|
||||||
B531EEDAFAF442B1E86A9931 /* LookupScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F22598772AD8B5EB6394F8 /* LookupScanView.swift */; };
|
B531EEDAFAF442B1E86A9931 /* LookupScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F22598772AD8B5EB6394F8 /* LookupScanView.swift */; };
|
||||||
D70CB183C868FF0143A6A5E7 /* DisplaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */; };
|
D70CB183C868FF0143A6A5E7 /* DisplaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */; };
|
||||||
|
D76F43864F9319FC8036A124 /* NFCTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA87A4690D179F825E67FC0 /* NFCTag.swift */; };
|
||||||
D7717F44D2CDB75DC2693B59 /* ObjectStockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5BB49FBEFF88768B0BC902 /* ObjectStockView.swift */; };
|
D7717F44D2CDB75DC2693B59 /* ObjectStockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5BB49FBEFF88768B0BC902 /* ObjectStockView.swift */; };
|
||||||
DFA55EF4ACA34537F445E998 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD2F9406BD0D4F0D30FED345 /* Session.swift */; };
|
DFA55EF4ACA34537F445E998 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD2F9406BD0D4F0D30FED345 /* Session.swift */; };
|
||||||
E216EFADBC6D6AC39C0547DF /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 31DDA98DEF7F8937A8719C80 /* AppAuth */; };
|
E216EFADBC6D6AC39C0547DF /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 31DDA98DEF7F8937A8719C80 /* AppAuth */; };
|
||||||
@@ -67,6 +69,7 @@
|
|||||||
4741D0E95875919C921945CF /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
4741D0E95875919C921945CF /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||||
4BF4E4F5524B15728CEEE116 /* Vorrania.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Vorrania.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
4BF4E4F5524B15728CEEE116 /* Vorrania.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = Vorrania.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5312AFE88B7284401AA7B0A3 /* ProductPhotoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPhotoView.swift; sourceTree = "<group>"; };
|
5312AFE88B7284401AA7B0A3 /* ProductPhotoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPhotoView.swift; sourceTree = "<group>"; };
|
||||||
|
5EA87A4690D179F825E67FC0 /* NFCTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCTag.swift; sourceTree = "<group>"; };
|
||||||
660ACEFA22BC77D52FBE736A /* AppDashboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDashboards.swift; sourceTree = "<group>"; };
|
660ACEFA22BC77D52FBE736A /* AppDashboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDashboards.swift; sourceTree = "<group>"; };
|
||||||
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViews.swift; sourceTree = "<group>"; };
|
69B59B9F69BC30451D45A3C7 /* ItemViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViews.swift; sourceTree = "<group>"; };
|
||||||
6F574168AA0F849D46C384EE /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = "<group>"; };
|
6F574168AA0F849D46C384EE /* ProductDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailView.swift; sourceTree = "<group>"; };
|
||||||
@@ -84,6 +87,7 @@
|
|||||||
AA022A27118BEEEC5912ECDC /* BestBeforeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BestBeforeText.swift; sourceTree = "<group>"; };
|
AA022A27118BEEEC5912ECDC /* BestBeforeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BestBeforeText.swift; sourceTree = "<group>"; };
|
||||||
AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettings.swift; sourceTree = "<group>"; };
|
AAC97A555856C6C5DD353E25 /* DisplaySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplaySettings.swift; sourceTree = "<group>"; };
|
||||||
AD2F9406BD0D4F0D30FED345 /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = "<group>"; };
|
AD2F9406BD0D4F0D30FED345 /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = "<group>"; };
|
||||||
|
C2B6163E03197495FAA1F298 /* NFCViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCViews.swift; sourceTree = "<group>"; };
|
||||||
C35D34107C8CBB8B7C6C6650 /* LocalOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOverview.swift; sourceTree = "<group>"; };
|
C35D34107C8CBB8B7C6C6650 /* LocalOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalOverview.swift; sourceTree = "<group>"; };
|
||||||
CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPicker.swift; sourceTree = "<group>"; };
|
CDD672F65352DA0D122EC9AE /* CategoryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPicker.swift; sourceTree = "<group>"; };
|
||||||
DC0DDD06332E567E70CA842F /* CheckOutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckOutView.swift; sourceTree = "<group>"; };
|
DC0DDD06332E567E70CA842F /* CheckOutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckOutView.swift; sourceTree = "<group>"; };
|
||||||
@@ -131,6 +135,8 @@
|
|||||||
18F22598772AD8B5EB6394F8 /* LookupScanView.swift */,
|
18F22598772AD8B5EB6394F8 /* LookupScanView.swift */,
|
||||||
1E86178BB91DBB86ADFB284B /* MasterDataViews.swift */,
|
1E86178BB91DBB86ADFB284B /* MasterDataViews.swift */,
|
||||||
314B1BB7220691A7423E8929 /* Models.swift */,
|
314B1BB7220691A7423E8929 /* Models.swift */,
|
||||||
|
5EA87A4690D179F825E67FC0 /* NFCTag.swift */,
|
||||||
|
C2B6163E03197495FAA1F298 /* NFCViews.swift */,
|
||||||
163F5DCB00387215F0EA1F21 /* NotificationScheduler.swift */,
|
163F5DCB00387215F0EA1F21 /* NotificationScheduler.swift */,
|
||||||
3731608B8D48960DC98912BC /* NotificationSettings.swift */,
|
3731608B8D48960DC98912BC /* NotificationSettings.swift */,
|
||||||
939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */,
|
939C7B7C2F2F58927ED5F2F1 /* NotificationSettingsView.swift */,
|
||||||
@@ -267,6 +273,8 @@
|
|||||||
B531EEDAFAF442B1E86A9931 /* LookupScanView.swift in Sources */,
|
B531EEDAFAF442B1E86A9931 /* LookupScanView.swift in Sources */,
|
||||||
96E5A2455B2EC26050E69232 /* MasterDataViews.swift in Sources */,
|
96E5A2455B2EC26050E69232 /* MasterDataViews.swift in Sources */,
|
||||||
6816C33381DF52A96E5BB303 /* Models.swift in Sources */,
|
6816C33381DF52A96E5BB303 /* Models.swift in Sources */,
|
||||||
|
D76F43864F9319FC8036A124 /* NFCTag.swift in Sources */,
|
||||||
|
10F43F9BA3CB785DEBBC625F /* NFCViews.swift in Sources */,
|
||||||
38133EE3EC920B462BDAA29F /* NotificationScheduler.swift in Sources */,
|
38133EE3EC920B462BDAA29F /* NotificationScheduler.swift in Sources */,
|
||||||
4B4A74E8A2964FF8F3D2CE02 /* NotificationSettings.swift in Sources */,
|
4B4A74E8A2964FF8F3D2CE02 /* NotificationSettings.swift in Sources */,
|
||||||
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */,
|
17032C3F3B89BAD6CB249443 /* NotificationSettingsView.swift in Sources */,
|
||||||
|
|||||||
+12
-5
@@ -50,11 +50,18 @@ targets:
|
|||||||
TARGETED_DEVICE_FAMILY: "1,2"
|
TARGETED_DEVICE_FAMILY: "1,2"
|
||||||
SWIFT_VERSION: "5.9"
|
SWIFT_VERSION: "5.9"
|
||||||
GENERATE_INFOPLIST_FILE: NO
|
GENERATE_INFOPLIST_FILE: NO
|
||||||
# Universal Links: …/i/<UID> öffnet direkt die App. Dafür muss im
|
# NFC: eingenaehte Tags an Einzelstuecken lesen und beschreiben – fuer
|
||||||
# Apple-Developer-Portal die Capability "Associated Domains" für die App-ID
|
# Kleidung, wo ein aufgeklebter QR die Waesche nicht uebersteht.
|
||||||
# aktiv sein (einmalig), sonst schlägt das Signieren fehl.
|
# Dafuer muss im Apple-Developer-Portal die Capability "Near Field
|
||||||
|
# Communication Tag Reading" fuer die App-ID aktiv sein (einmalig), sonst
|
||||||
|
# schlaegt das Signieren fehl.
|
||||||
|
#
|
||||||
|
# Associated Domains stand hier bis 2026-09 noch drin, obwohl f981f41 die
|
||||||
|
# Universal Links entfernt hat: XcodeGen erzeugt Sources/Vorrania.entitlements
|
||||||
|
# aus diesem Block und schrieb den Eintrag bei jedem "generate" zurueck. Die
|
||||||
|
# Domain existiert laengst nicht mehr, jede Installation fragte sie vergeblich ab.
|
||||||
entitlements:
|
entitlements:
|
||||||
path: Sources/Vorrania.entitlements
|
path: Sources/Vorrania.entitlements
|
||||||
properties:
|
properties:
|
||||||
com.apple.developer.associated-domains:
|
com.apple.developer.nfc.readersession.formats:
|
||||||
- applinks:vorrania-ch.scarriffle.com
|
- NDEF
|
||||||
|
|||||||
Reference in New Issue
Block a user