App: NFC-Fehler kurz statt "Stack Error", Knopf weg nach Erfolg

Apples localizedDescription kam unveraendert durch und lieferte beim
Beschreiben von Tags wiederholt "Stack Error" - ein Text, der niemandem
sagt, was zu tun ist. Jetzt uebersetzt NFCDienst.kurz die CoreNFC-Codes
in kurze Meldungen ("Tag zu frueh entfernt.", "NFC gerade belegt."); ein
unbekannter Code behaelt seine Nummer, damit der fehlende Fall beim
naechsten Auftreten feststeht.

Die Meldung steht jetzt auch in Apples Systemblatt: im Fehlerfall wurde
die Sitzung wortlos beendet, deshalb las man sie erst danach in der App.

Nach einem erfolgreichen Schreibvorgang verschwinden Knopf und
Schreibschutz-Schalter - sie standen vorher weiter da und waren aktiv,
ein weiterer Tipp hat das Tag ein zweites Mal beschrieben. Stattdessen
steht dort jetzt "Fertig".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-20 21:41:35 +02:00
co-authored by Claude Opus 5
parent 04f8c1fccd
commit 8c752e3d1f
2 changed files with 79 additions and 25 deletions
+53 -9
View File
@@ -99,6 +99,48 @@ final class NFCDienst: NSObject {
} }
} }
/// Kurzfassung eines CoreNFC-Fehlers.
///
/// Apples ``localizedDescription`` ist hier unbrauchbar – sie liefert Dinge
/// wie „Stack Error", die niemandem sagen, was zu tun ist. Fast alle Faelle
/// sind voruebergehend: Tag zu frueh weg, Funkkontakt verloren, Stack belegt.
///
/// Ein unbekannter Code behaelt seine Nummer. Damit steht beim naechsten
/// Auftreten sofort fest, welcher Fall hier noch fehlt.
static func kurz(_ fehler: Error) -> String {
guard let code = (fehler as? NFCReaderError)?.code else {
return "Fehlgeschlagen."
}
switch code {
case .readerTransceiveErrorTagConnectionLost:
return "Tag zu früh entfernt."
case .readerTransceiveErrorRetryExceeded:
return "Keine Antwort vom Tag."
case .readerTransceiveErrorTagResponseError:
return "Tag antwortet fehlerhaft."
case .readerTransceiveErrorTagNotConnected:
return "Kein Tag verbunden."
case .readerSessionInvalidationErrorSessionTimeout:
return "Zeit abgelaufen."
case .readerSessionInvalidationErrorSystemIsBusy:
return "NFC gerade belegt."
case .readerSessionInvalidationErrorSessionTerminatedUnexpectedly:
return "Sitzung abgebrochen."
case .ndefReaderSessionErrorTagUpdateFailure:
return "Schreiben fehlgeschlagen."
case .ndefReaderSessionErrorTagNotWritable:
return "Tag nicht beschreibbar."
case .readerErrorRadioDisabled:
return "NFC ist aus."
case .readerErrorSecurityViolation:
return "NFC nicht erlaubt."
case .readerErrorUnsupportedFeature:
return "Tag-Typ nicht unterstützt."
default:
return "Fehlgeschlagen (Code \(code.rawValue))."
}
}
private enum Auftrag { private enum Auftrag {
case lesen case lesen
case schreiben(nutzlast: String, sperren: Bool) case schreiben(nutzlast: String, sperren: Bool)
@@ -155,8 +197,10 @@ final class NFCDienst: NSObject {
if let meldung { if let meldung {
session?.alertMessage = meldung session?.alertMessage = meldung
session?.invalidate() session?.invalidate()
} else if case .failure = ergebnis { } else if case let .failure(fehler) = ergebnis {
session?.invalidate() // Mit Text: Ohne ihn schliesst sich Apples Blatt wortlos und die
// Meldung steht erst danach in der App.
session?.invalidate(errorMessage: fehler.localizedDescription)
} }
session = nil session = nil
fortsetzung.resume(with: ergebnis) fortsetzung.resume(with: ergebnis)
@@ -166,7 +210,7 @@ final class NFCDienst: NSObject {
private func lesen(_ tag: NFCNDEFTag) { private func lesen(_ tag: NFCNDEFTag) {
tag.readNDEF { nachricht, fehler in tag.readNDEF { nachricht, fehler in
if let fehler { if let fehler {
self.fertig(.failure(Fehler.system(fehler.localizedDescription))) self.fertig(.failure(Fehler.system(Self.kurz(fehler))))
return return
} }
guard let text = nachricht?.records.compactMap(Self.text(aus:)).first, !text.isEmpty else { guard let text = nachricht?.records.compactMap(Self.text(aus:)).first, !text.isEmpty else {
@@ -187,7 +231,7 @@ final class NFCDienst: NSObject {
tag.queryNDEFStatus { status, kapazitaet, fehler in tag.queryNDEFStatus { status, kapazitaet, fehler in
if let fehler { if let fehler {
self.fertig(.failure(Fehler.system(fehler.localizedDescription))) self.fertig(.failure(Fehler.system(Self.kurz(fehler))))
return return
} }
switch status { switch status {
@@ -210,7 +254,7 @@ final class NFCDienst: NSObject {
tag.writeNDEF(nachricht) { fehler in tag.writeNDEF(nachricht) { fehler in
if let fehler { if let fehler {
self.fertig(.failure(Fehler.system(fehler.localizedDescription))) self.fertig(.failure(Fehler.system(Self.kurz(fehler))))
return return
} }
guard sperren else { guard sperren else {
@@ -224,8 +268,8 @@ final class NFCDienst: NSObject {
// Das gehört gesagt, statt den ganzen Vorgang als // Das gehört gesagt, statt den ganzen Vorgang als
// gescheitert auszugeben. // gescheitert auszugeben.
self.fertig(.failure(Fehler.system( self.fertig(.failure(Fehler.system(
"Tag beschrieben, aber das Sperren ist fehlgeschlagen: " "Tag beschrieben, Sperren fehlgeschlagen: "
+ fehler.localizedDescription))) + Self.kurz(fehler))))
return return
} }
AudioServicesPlaySystemSound(1057) AudioServicesPlaySystemSound(1057)
@@ -274,7 +318,7 @@ extension NFCDienst: NFCTagReaderSessionDelegate {
} }
session.connect(to: ersteser) { fehler in session.connect(to: ersteser) { fehler in
if let fehler { if let fehler {
self.fertig(.failure(Fehler.system(fehler.localizedDescription))) self.fertig(.failure(Fehler.system(Self.kurz(fehler))))
return return
} }
switch self.auftrag { switch self.auftrag {
@@ -293,6 +337,6 @@ extension NFCDienst: NFCTagReaderSessionDelegate {
let abbruch = (error as? NFCReaderError)?.code let abbruch = (error as? NFCReaderError)?.code
== .readerSessionInvalidationErrorUserCanceled == .readerSessionInvalidationErrorUserCanceled
self.session = nil self.session = nil
fertig(.failure(abbruch ? Fehler.abgebrochen : Fehler.system(error.localizedDescription))) fertig(.failure(abbruch ? Fehler.abgebrochen : Fehler.system(Self.kurz(error))))
} }
} }
+26 -16
View File
@@ -33,30 +33,40 @@ struct NFCSchreibenView: View {
Text("Auf das Tag wird geschrieben: \(nutzlast)") Text("Auf das Tag wird geschrieben: \(nutzlast)")
} }
Section { // Nach dem Erfolg hat das Tag seinen Inhalt. Knopf und Schalter
Toggle("Tag dauerhaft schreibschützen", isOn: $sperren) // stehenzulassen hiesse, es noch einmal beschreiben zu wollen.
} footer: { if !erfolg {
if sperren { Section {
Text("Das lässt sich nicht rückgängig machen. Das Tag gehört danach " Toggle("Tag dauerhaft schreibschützen", isOn: $sperren)
+ "für immer zu diesem Einzelstück und kann nicht neu beschrieben werden.") } footer: {
.foregroundStyle(.orange) if sperren {
} else { Text("Das lässt sich nicht rückgängig machen. Das Tag gehört danach "
Text("Ohne Schreibschutz kannst du das Tag später einem anderen " + "für immer zu diesem Einzelstück und kann nicht neu beschrieben werden.")
+ "Einzelstück zuweisen.") .foregroundStyle(.orange)
} else {
Text("Ohne Schreibschutz kannst du das Tag später einem anderen "
+ "Einzelstück zuweisen.")
}
} }
}
Section { Section {
Button(laeuft ? "Tag anhalten…" : "Tag beschreiben") { Button(laeuft ? "Tag anhalten…" : "Tag beschreiben") {
Task { await schreiben() } Task { await schreiben() }
}
.disabled(laeuft)
} }
.disabled(laeuft)
} }
if erfolg { if erfolg {
Section { Section {
Label("Tag beschrieben.", systemImage: "checkmark.circle") Label(sperren ? "Tag beschrieben und gesperrt." : "Tag beschrieben.",
systemImage: "checkmark.circle")
.foregroundStyle(.green) .foregroundStyle(.green)
// Hier ist nichts mehr zu tun; „Fertig" gehoert deshalb in
// den Inhalt und nicht nur oben in die Leiste. Ohne das
// stuende man im Anlege-Ablauf vor einem fast leeren Blatt.
Button("Fertig") { if let onFertig { onFertig() } else { dismiss() } }
.buttonStyle(.borderedProminent)
} }
} }
if let fehler { if let fehler {