From 140b81798c0c1a563a1591045f6abf3e2026adbd Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sat, 12 Sep 2026 13:48:40 +0200 Subject: [PATCH] NFC: Entitlement TAG statt NDEF, Tag-Session statt NDEF-Session Der Upload nach App Store Connect scheiterte an der Entitlement-Pruefung: "NDEF is disallowed" und "TAG is missing in the entitlement". Apple hat den Wert NDEF zugunsten von TAG abgeloest; mit dem SDK von iOS 26/27 wird er abgelehnt. TAG ist der Wert fuer NFCTagReaderSession, deshalb laeuft der Lese- und Schreibweg jetzt darueber statt ueber NFCNDEFReaderSession. Die NDEF-Befehle bleiben dabei vollstaendig erhalten: alle vier Tag-Protokolle (MiFare, ISO7816, ISO15693, FeliCa) erben laut SDK-Header von NFCNDEFTag, also gibt es weiterhin queryNDEFStatus, readNDEF, writeNDEF und writeLock. Gepollt wird iso14443 (die ueblichen NTAG-Aufkleber) und iso15693 (Waeschetags mancher Hersteller). Die eigene Klasse hiess NFCTag und kollidierte damit mit dem gleichnamigen CoreNFC-Protokoll, das der neue Weg braucht - sie heisst jetzt NFCDienst. Info.plist enthaelt ausserdem die Neuformatierung durch Xcode (Schluessel sortiert, Kommentare entfernt). Inhaltlich unveraendert. Co-Authored-By: Claude Opus 5 --- ios/Sources/Info.plist | 204 +++++++++++++----------------- ios/Sources/ItemViews.swift | 2 +- ios/Sources/NFCTag.swift | 73 +++++++---- ios/Sources/NFCViews.swift | 10 +- ios/Sources/Vorrania.entitlements | 2 +- ios/project.yml | 6 +- 6 files changed, 147 insertions(+), 150 deletions(-) diff --git a/ios/Sources/Info.plist b/ios/Sources/Info.plist index f9fbd87..d4c5518 100644 --- a/ios/Sources/Info.plist +++ b/ios/Sources/Info.plist @@ -2,123 +2,91 @@ - - - CFBundleDevelopmentRegion - de - CFBundleLocalizations - - de - - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - - CFBundleDisplayName - Vorrania - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - - LSApplicationCategoryType - public.app-category.utilities - - - ITSAppUsesNonExemptEncryption - - - UILaunchScreen - - - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - - NSCameraUsageDescription - Die Kamera wird zum Scannen von Barcodes beim Ein- und Auslagern verwendet. - - - NFCReaderUsageDescription - NFC wird zum Lesen und Beschreiben der Tags an Einzelstücken verwendet. - - - NSAppTransportSecurity - - NSAllowsLocalNetworking - - - - - UIBackgroundModes - - fetch - - BGTaskSchedulerPermittedIdentifiers - - com.scarriffle.vorrania.refresh - - - - UIApplicationShortcutItems - - - UIApplicationShortcutItemType - com.scarriffle.vorrania.checkin - UIApplicationShortcutItemTitle - Einlagern - UIApplicationShortcutItemIconType - UIApplicationShortcutIconTypeAdd - - - UIApplicationShortcutItemType - com.scarriffle.vorrania.checkout - UIApplicationShortcutItemTitle - Auslagern - UIApplicationShortcutItemIconType - UIApplicationShortcutIconTypeRemove - - - - - CFBundleURLTypes - - - CFBundleURLName - com.scarriffle.vorrania - CFBundleURLSchemes - - vorrania - - - + BGTaskSchedulerPermittedIdentifiers + + com.scarriffle.vorrania.refresh + + CFBundleDevelopmentRegion + de + CFBundleDisplayName + Vorrania + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + de + + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleURLName + com.scarriffle.vorrania + CFBundleURLSchemes + + vorrania + + + + CFBundleVersion + 1 + ITSAppUsesNonExemptEncryption + + LSApplicationCategoryType + public.app-category.utilities + NFCReaderUsageDescription + NFC wird zum Lesen und Beschreiben der Tags an Einzelstücken verwendet. + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + NSCameraUsageDescription + Die Kamera wird zum Scannen von Barcodes beim Ein- und Auslagern verwendet. + UIApplicationShortcutItems + + + UIApplicationShortcutItemIconType + UIApplicationShortcutIconTypeAdd + UIApplicationShortcutItemTitle + Einlagern + UIApplicationShortcutItemType + com.scarriffle.vorrania.checkin + + + UIApplicationShortcutItemIconType + UIApplicationShortcutIconTypeRemove + UIApplicationShortcutItemTitle + Auslagern + UIApplicationShortcutItemType + com.scarriffle.vorrania.checkout + + + UIBackgroundModes + + fetch + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + diff --git a/ios/Sources/ItemViews.swift b/ios/Sources/ItemViews.swift index 37e116c..55a6695 100644 --- a/ios/Sources/ItemViews.swift +++ b/ios/Sources/ItemViews.swift @@ -214,7 +214,7 @@ struct ItemEditView: View { 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 { + if NFCDienst.verfuegbar { Button { nfcShown = true } label: { Label("NFC-Tag beschreiben", systemImage: "wave.3.right") } diff --git a/ios/Sources/NFCTag.swift b/ios/Sources/NFCTag.swift index dc1dfb4..9d131e7 100644 --- a/ios/Sources/NFCTag.swift +++ b/ios/Sources/NFCTag.swift @@ -50,17 +50,25 @@ func einzelstueckCode(aus roh: String, nackteUidErlauben: Bool = false) -> Strin /// aussen sieht das hier wie ein normales `await` aus, damit die Views genauso /// aussehen wie beim Kamera-Scan. /// +/// **Warum `NFCTagReaderSession` und nicht `NFCNDEFReaderSession`:** Seit dem +/// SDK von iOS 26/27 lehnt die App-Store-Prüfung den Entitlement-Wert `NDEF` ab +/// („NDEF is disallowed") und verlangt `TAG`. `TAG` ist der Wert für diese +/// Sitzungsart. Die NDEF-Befehle bleiben trotzdem verfügbar: alle vier +/// Tag-Protokolle (MiFare, ISO7816, ISO15693, FeliCa) erben von `NFCNDEFTag`, +/// also gibt es weiterhin `queryNDEFStatus`, `readNDEF`, `writeNDEF` und +/// `writeLock`. +/// /// 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 { +final class NFCDienst: 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 } + static var verfuegbar: Bool { NFCTagReaderSession.readingAvailable } enum Fehler: LocalizedError { case nichtVerfuegbar @@ -97,38 +105,42 @@ final class NFCTag: NSObject { } private let queue = DispatchQueue(label: "com.scarriffle.vorrania.nfc") - private var session: NFCNDEFReaderSession? + private var session: NFCTagReaderSession? private var auftrag: Auftrag = .lesen private var weiter: CheckedContinuation? /// 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? + private var festhalten: NFCDienst? // 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.") + try await NFCDienst().starten(.lesen) } /// 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." - ) + _ = try await NFCDienst().starten(.schreiben(nutzlast: nutzlast, sperren: sperren)) } // MARK: Ablauf - private func starten(_ auftrag: Auftrag, hinweis: String) async throws -> String { + private func starten(_ auftrag: Auftrag) async throws -> String { guard Self.verfuegbar else { throw Fehler.nichtVerfuegbar } self.auftrag = auftrag return try await withCheckedThrowingContinuation { fortsetzung in + // iso14443 deckt die üblichen NTAG-/MIFARE-Aufkleber ab, iso15693 die + // Wäschetags, die manche Hersteller stattdessen verbauen. + guard let s = NFCTagReaderSession( + pollingOption: [.iso14443, .iso15693], delegate: self, queue: queue + ) else { + fortsetzung.resume(throwing: Fehler.nichtVerfuegbar) + return + } weiter = fortsetzung festhalten = self - let s = NFCNDEFReaderSession(delegate: self, queue: queue, invalidateAfterFirstRead: false) - s.alertMessage = hinweis + s.alertMessage = "Halte das Tag an die Oberseite des iPhones." session = s s.begin() } @@ -230,38 +242,51 @@ final class NFCTag: NSObject { if let text = record.wellKnownTypeTextPayload().0 { return text } return String(data: record.payload, encoding: .utf8) } + + /// Alle vier Tag-Arten erben von `NFCNDEFTag` – die NDEF-Befehle gibt es + /// also unabhängig davon, welche Technik das Tag verwendet. + private static func ndef(aus tag: NFCTag) -> NFCNDEFTag? { + switch tag { + case let .miFare(t): return t + case let .iso7816(t): return t + case let .iso15693(t): return t + case let .feliCa(t): return t + @unknown default: return nil + } + } } -// MARK: - NFCNDEFReaderSessionDelegate +// MARK: - NFCTagReaderSessionDelegate -extension NFCTag: NFCNDEFReaderSessionDelegate { +extension NFCDienst: NFCTagReaderSessionDelegate { - /// 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 tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {} - func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [NFCNDEFTag]) { - guard tags.count == 1, let tag = tags.first else { + func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + guard tags.count == 1, let ersteser = tags.first else { session.alertMessage = "Mehrere Tags erkannt. Bitte nur eines anhalten." session.restartPolling() return } - session.connect(to: tag) { fehler in + guard let ndef = Self.ndef(aus: ersteser) else { + fertig(.failure(Fehler.nichtBeschreibbar)) + return + } + session.connect(to: ersteser) { fehler in if let fehler { self.fertig(.failure(Fehler.system(fehler.localizedDescription))) return } switch self.auftrag { case .lesen: - self.lesen(tag) + self.lesen(ndef) case let .schreiben(nutzlast, sperren): - self.schreiben(tag, nutzlast: nutzlast, sperren: sperren) + self.schreiben(ndef, nutzlast: nutzlast, sperren: sperren) } } } - func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) { + func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { // Hat der Vorgang schon ein Ergebnis geliefert, ist das hier nur noch das // Aufräumen danach. guard weiter != nil else { return } diff --git a/ios/Sources/NFCViews.swift b/ios/Sources/NFCViews.swift index 97416fa..8157c2a 100644 --- a/ios/Sources/NFCViews.swift +++ b/ios/Sources/NFCViews.swift @@ -71,9 +71,9 @@ struct NFCSchreibenView: View { erfolg = false defer { laeuft = false } do { - try await NFCTag.schreiben(nutzlast, sperren: sperren) + try await NFCDienst.schreiben(nutzlast, sperren: sperren) erfolg = true - } catch NFCTag.Fehler.abgebrochen { + } catch NFCDienst.Fehler.abgebrochen { // Kein Fehler, sondern eine Entscheidung – nichts anzeigen. } catch { fehler = error.localizedDescription @@ -93,7 +93,7 @@ struct NFCLesenButton: View { @State private var laeuft = false var body: some View { - if NFCTag.verfuegbar { + if NFCDienst.verfuegbar { Button { Task { await lesen() } } label: { @@ -107,7 +107,7 @@ struct NFCLesenButton: View { laeuft = true defer { laeuft = false } do { - let roh = try await NFCTag.lesen() + let roh = try await NFCDienst.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 { @@ -115,7 +115,7 @@ struct NFCLesenButton: View { return } onUid(uid) - } catch NFCTag.Fehler.abgebrochen { + } catch NFCDienst.Fehler.abgebrochen { // Nutzer hat abgebrochen – keine Meldung. } catch { onFehler(error.localizedDescription) diff --git a/ios/Sources/Vorrania.entitlements b/ios/Sources/Vorrania.entitlements index 5f7b942..2bb4dee 100644 --- a/ios/Sources/Vorrania.entitlements +++ b/ios/Sources/Vorrania.entitlements @@ -4,7 +4,7 @@ com.apple.developer.nfc.readersession.formats - NDEF + TAG diff --git a/ios/project.yml b/ios/project.yml index 809b5ff..f76e23a 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -63,5 +63,9 @@ targets: entitlements: path: Sources/Vorrania.entitlements properties: + # "TAG", nicht "NDEF": Die App-Store-Pruefung lehnt NDEF seit dem + # SDK von iOS 26/27 ab ("NDEF is disallowed") und verlangt TAG. Deshalb + # laeuft der Code ueber NFCTagReaderSession; die NDEF-Befehle gibt es + # dort weiterhin, weil alle Tag-Protokolle von NFCNDEFTag erben. com.apple.developer.nfc.readersession.formats: - - NDEF + - TAG