diff --git a/Examples/TRETNFCKitExample.xcodeproj/project.pbxproj b/Examples/TRETNFCKitExample.xcodeproj/project.pbxproj index 6db9bff9..63293799 100644 --- a/Examples/TRETNFCKitExample.xcodeproj/project.pbxproj +++ b/Examples/TRETNFCKitExample.xcodeproj/project.pbxproj @@ -322,7 +322,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -356,7 +356,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Examples/TRETNFCKitExample/NFCFeliCaTagReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCFeliCaTagReaderExampleView.swift index b52f9f2e..691edc23 100644 --- a/Examples/TRETNFCKitExample/NFCFeliCaTagReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCFeliCaTagReaderExampleView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_FeliCa struct NFCFeliCaTagReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCTagReaderSession? var body: some View { List { @@ -24,8 +26,14 @@ struct NFCFeliCaTagReaderExampleView: View { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCTagReaderSession(pollingOption: .iso18092) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .feliCaTagReader( isPresented: $isPresented, @@ -47,6 +55,40 @@ struct NFCFeliCaTagReaderExampleView: View { return .success } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCTagReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let tags): + do { + let tag = tags.first! + guard case .feliCa(let feliCaTag) = tag else { + throw NFCReaderError(.readerErrorInvalidParameter) + } + try await readerSession.connect(to: tag) + let (idm, systemCode) = try await feliCaTag.polling(systemCode: Data([0xFE, 0x00]), requestCode: .systemCode, timeSlot: .max1) + readerSession.alertMessage = "\(systemCode as NSData)\n\(idm as NSData)" + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionCreationFailed(let reason): + print(reason) + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("FeliCa") } } diff --git a/Examples/TRETNFCKitExample/NFCISO15693TagReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCISO15693TagReaderExampleView.swift index a257d318..f24698c6 100644 --- a/Examples/TRETNFCKitExample/NFCISO15693TagReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCISO15693TagReaderExampleView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_ISO15693 struct NFCISO15693TagReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCTagReaderSession? var body: some View { List { @@ -24,8 +26,14 @@ struct NFCISO15693TagReaderExampleView: View { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCTagReaderSession(pollingOption: .iso15693) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .iso15693TagReader( isPresented: $isPresented, @@ -46,6 +54,39 @@ struct NFCISO15693TagReaderExampleView: View { return .success } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCTagReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let tags): + do { + let tag = tags.first! + guard case .iso15693(let iso15693Tag) = tag else { + throw NFCReaderError(.readerErrorInvalidParameter) + } + try await readerSession.connect(to: tag) + readerSession.alertMessage = "\(iso15693Tag.identifier as NSData)" + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionCreationFailed(let reason): + print(reason) + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("ISO 15693-compatible") } } diff --git a/Examples/TRETNFCKitExample/NFCISO7816TagReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCISO7816TagReaderExampleView.swift index 0bde504c..fd916b4c 100644 --- a/Examples/TRETNFCKitExample/NFCISO7816TagReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCISO7816TagReaderExampleView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_ISO7816 struct NFCISO7816TagReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCTagReaderSession? var body: some View { List { @@ -24,8 +26,14 @@ struct NFCISO7816TagReaderExampleView: View { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCTagReaderSession(pollingOption: .iso14443) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .iso7816TagReader( isPresented: $isPresented, @@ -46,6 +54,39 @@ struct NFCISO7816TagReaderExampleView: View { return .success } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCTagReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let tags): + do { + let tag = tags.first! + guard case .iso7816(let iso7816Tag) = tag else { + throw NFCReaderError(.readerErrorInvalidParameter) + } + try await readerSession.connect(to: tag) + readerSession.alertMessage = "\(iso7816Tag.identifier as NSData)" + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionCreationFailed(let reason): + print(reason) + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("ISO 7816-compatible") } } diff --git a/Examples/TRETNFCKitExample/NFCMiFareTagReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCMiFareTagReaderExampleView.swift index b5e10830..7569a769 100644 --- a/Examples/TRETNFCKitExample/NFCMiFareTagReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCMiFareTagReaderExampleView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_MiFare struct NFCMiFareTagReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCTagReaderSession? var body: some View { List { @@ -24,8 +26,14 @@ struct NFCMiFareTagReaderExampleView: View { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCTagReaderSession(pollingOption: .iso14443) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .miFareTagReader( isPresented: $isPresented, @@ -46,6 +54,39 @@ struct NFCMiFareTagReaderExampleView: View { return .success } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCTagReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let tags): + do { + let tag = tags.first! + guard case .miFare(let miFareTag) = tag else { + throw NFCReaderError(.readerErrorInvalidParameter) + } + try await readerSession.connect(to: tag) + readerSession.alertMessage = "\(miFareTag.identifier as NSData)" + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionCreationFailed(let reason): + print(reason) + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("MiFare") } } diff --git a/Examples/TRETNFCKitExample/NFCNDEFMessageReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCNDEFMessageReaderExampleView.swift index 6d8d0ab5..ce84ad46 100644 --- a/Examples/TRETNFCKitExample/NFCNDEFMessageReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCNDEFMessageReaderExampleView.swift @@ -6,26 +6,34 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_NDEFMessage struct NFCNDEFMessageReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCNDEFMessageReaderSession? var body: some View { List { Button { isPresented = true } label: { - Text("Read (using view modifier)") + Text("Read (using reader view modifier)") } Button { Task { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCNDEFMessageReaderSession(invalidateAfterFirstRead: false) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .nfcNDEFMessageReader( isPresented: $isPresented, @@ -45,6 +53,29 @@ struct NFCNDEFMessageReaderExampleView: View { return .success(alertMessage: "Done!") } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCNDEFMessageReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let messages): + print(messages) + readerSession.alertMessage = "Done!" + readerSession.stop() + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("NDEF Messages") } } diff --git a/Examples/TRETNFCKitExample/NFCNDEFTagReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCNDEFTagReaderExampleView.swift index b19a71a8..503725b4 100644 --- a/Examples/TRETNFCKitExample/NFCNDEFTagReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCNDEFTagReaderExampleView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_NDEFTag struct NFCNDEFTagReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCNDEFTagReaderSession? var body: some View { List { @@ -24,8 +26,14 @@ struct NFCNDEFTagReaderExampleView: View { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCNDEFTagReaderSession(invalidateAfterFirstRead: false) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .nfcNDEFTagReader( isPresented: $isPresented, @@ -47,6 +55,35 @@ struct NFCNDEFTagReaderExampleView: View { return .success } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCNDEFTagReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let tags): + do { + let tag = tags.first! + try await readerSession.connect(to: tag) + let message = try await tag.readNDEF() + readerSession.alertMessage = "\(message)" + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("NDEF Tag") } } diff --git a/Examples/TRETNFCKitExample/NFCNativeTagReaderExampleView.swift b/Examples/TRETNFCKitExample/NFCNativeTagReaderExampleView.swift index fc837fd6..1435f870 100644 --- a/Examples/TRETNFCKitExample/NFCNativeTagReaderExampleView.swift +++ b/Examples/TRETNFCKitExample/NFCNativeTagReaderExampleView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import TRETNFCKit_Async import TRETNFCKit_NativeTag struct NFCNativeTagReaderExampleView: View { @State private var isPresented = false @ObservedObject var viewModel = ViewModel() + @State private var readerSession: AsyncNFCTagReaderSession? var body: some View { List { @@ -24,8 +26,14 @@ struct NFCNativeTagReaderExampleView: View { try await viewModel.read() } } label: { - Text("Read") + Text("Read (using reader)") } + Button { + readerSession = AsyncNFCTagReaderSession(pollingOption: [.iso14443, .iso15693, .iso18092]) + } label: { + Text("Read (using async stream)") + } + .disabled(readerSession != nil) } .nfcNativeTagReader( isPresented: $isPresented, @@ -58,6 +66,47 @@ struct NFCNativeTagReaderExampleView: View { return .success } ) + .task(id: readerSession == nil) { + defer { readerSession = nil } + guard let readerSession else { return } + guard AsyncNFCTagReaderSession.readingAvailable else { return } + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + break + case .sessionBecomeActive: + break + case .sessionDetected(let tags): + do { + let tag = tags.first! + try await readerSession.connect(to: tag) + switch tag { + case .feliCa(let feliCaTag): + readerSession.alertMessage = "FeliCa\n\(feliCaTag.currentIDm as NSData)" + case .iso7816(let iso7816Tag): + readerSession.alertMessage = "ISO14443-4 type A / B tag with ISO7816\n\(iso7816Tag.identifier as NSData)" + case .iso15693(let iso15693Tag): + readerSession.alertMessage = "ISO 15693\n\(iso15693Tag.identifier as NSData)" + case .miFare(let miFareTag): + readerSession.alertMessage = "MiFare technology tag (MIFARE Plus, UltraLight, DESFire) base on ISO14443\n\(miFareTag.identifier as NSData)" + @unknown default: + readerSession.alertMessage = "Unknown tag." + } + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionCreationFailed(let reason): + print(reason) + case .sessionInvalidated(let reason): + print(reason) + } + } + } .navigationTitle("Multiple") } } diff --git a/Package.swift b/Package.swift index 0d4fc601..680486bf 100644 --- a/Package.swift +++ b/Package.swift @@ -50,6 +50,7 @@ let assertServices = add(moduleName: "AssertServices", includesTest: false) let infoPListChecker = add(moduleName: "InfoPListChecker", includesTest: false) // MARK: - Modules - Primary +let async = add(moduleName: "Async", includesTest: false) let core = add(moduleName: "Core", dependencies: [assertServices, infoPListChecker], includesTest: true) let nativeTag = add(moduleName: "NativeTag", dependencies: [core], includesTest: true) add(moduleName: "NDEFMessage", dependencies: [core], includesTest: true) diff --git a/README.md b/README.md index 23416c5d..a28c4f78 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,28 @@ A wrapper for Core NFC and a useful helper when using NFC, leveraging Swift feat [![Xcode Build & Test](https://github.com/treastrain/TRETJapanNFCReader/actions/workflows/xcodebuild.yml/badge.svg?branch=tretnfckit-main)](https://github.com/treastrain/TRETJapanNFCReader/actions/workflows/xcodebuild.yml) [![Example App Build](https://github.com/treastrain/TRETJapanNFCReader/actions/workflows/xcodebuild_for_example_app.yml/badge.svg?branch=tretnfckit-main)](https://github.com/treastrain/TRETJapanNFCReader/actions/workflows/xcodebuild_for_example_app.yml) -# Usage -- ✅ No delegation pattern - - When using Core NFC directly, it is usually a delegation pattern. In this case, this is unsafe because it is possible to forget to call a necessary command. - - By using this wrapper, it can be converted to a closure pattern compatible with Swift Concurrency, and the Swift syntax prevents forgetting to call the necessary commands. -- ✅ Support Swift Concurrency (async/await, Actor, Sendable) - - It contains an Actor-wrapped [`NFCNDEFReaderSession`](https://developer.apple.com/documentation/corenfc/nfcndefreadersession)\, [`NFCTagReaderSession`](https://developer.apple.com/documentation/corenfc/nfctagreadersession)\, and [`NFCVASReaderSession`](https://developer.apple.com/documentation/corenfc/nfcvasreadersession)\, so they are safe for concurrency. +# Features +- ✅ (For beginners) A high-level wrapper for reader that can prevent forgetting to call necessary commands + - `FeliCaTagReader`: FeliCa (NFC-F) + - `ISO7816TagReader`: ISO 7816-compatible (NFC-A/B) + - `ISO15693TagReader`: ISO 15693-compatible (NFC-V) + - `MiFareTagReader`: MiFare (MIFARE Plus, UltraLight, DESFire) base on ISO 14443 (NFC-A) + - `NFCReader`: (for use directly) + - `NFCReader` + - `NFCReader` + - `NFCReader` +- ✅ (For experts) A low-level wrapper for reader session that includes a conversion to the `AsyncSequence` pattern from the delegation pattern originally provided by Core NFC + - The asynchronous sequence is implemented in the following wrappers, which is very similar to [`CardSession.eventStream`](https://developer.apple.com/documentation/corenfc/cardsession/4318517-eventstream) (provided by Core NFC in iOS 17.4+, which allows communication with HCE-based NFC readers based on ISO 7816-4) + - `AsyncNFCNDEFMessageReaderSession` (for [`NFCNDEFReaderSession`](https://developer.apple.com/documentation/corenfc/nfcndefreadersession) with [`NFCNDEFMessage`](https://developer.apple.com/documentation/corenfc/nfcndeftag)s) + - `AsyncNFCNDEFTagReaderSession` (for [`NFCNDEFReaderSession`](https://developer.apple.com/documentation/corenfc/nfcndefreadersession) with [`NFCNDEFTag`](https://developer.apple.com/documentation/corenfc/nfcndefmessage)s) + - `AsyncNFCTagReaderSession` (for [`NFCTagReaderSession`](https://developer.apple.com/documentation/corenfc/nfctagreadersession)) + - `AsyncNFCVASReaderSession` (for [`NFCVASReaderSession`](https://developer.apple.com/documentation/corenfc/nfcvasreadersession)) - ✅ Support SwiftUI -## Native Tags (FeliCa (NFC-F), ISO 7816-compatible (NFC-A/B), ISO 15693-compatible (NFC-V), MiFare (NFC-A)) -### FeliCa (NFC-F) +# Usage +## High-level wrappers +### Native Tags (FeliCa (NFC-F), ISO 7816-compatible (NFC-A/B), ISO 15693-compatible (NFC-V), MiFare (NFC-A)) +#### FeliCa (NFC-F) ```swift import TRETNFCKit_FeliCa @@ -45,7 +57,7 @@ try await reader.read( } ``` -#### for SwiftUI +##### for SwiftUI ```swift import SwiftUI import TRETNFCKit_FeliCa @@ -64,7 +76,7 @@ Text("some view") ) ``` -### ISO 7816-compatible (NFC-A/B) +#### ISO 7816-compatible (NFC-A/B) ```swift import TRETNFCKit_ISO7816 @@ -81,7 +93,7 @@ try await reader.read( } ``` -#### for SwiftUI +##### for SwiftUI ```swift import SwiftUI import TRETNFCKit_ISO7816 @@ -100,7 +112,7 @@ Text("some view") ) ``` -### ISO 15693-compatible (NFC-V) +#### ISO 15693-compatible (NFC-V) ```swift import TRETNFCKit_ISO15693 @@ -117,7 +129,7 @@ try await reader.read( } ``` -#### for SwiftUI +##### for SwiftUI ```swift import SwiftUI import TRETNFCKit_ISO15693 @@ -136,7 +148,7 @@ Text("some view") ) ``` -### MiFare (MIFARE Plus, UltraLight, DESFire) base on ISO 14443 (NFC-A) +#### MiFare (MIFARE Plus, UltraLight, DESFire) base on ISO 14443 (NFC-A) ```swift import TRETNFCKit_MiFare @@ -153,7 +165,7 @@ try await reader.read( } ``` -#### for SwiftUI +##### for SwiftUI ```swift import SwiftUI import TRETNFCKit_MiFare @@ -172,7 +184,7 @@ Text("some view") ) ``` -### Use directly +#### Use directly ```swift import TRETNFCKit_NativeTag @@ -198,7 +210,7 @@ try await reader.read( ) ``` -#### for SwiftUI +##### for SwiftUI ```swift import SwiftUI import TRETNFCKit_NativeTag @@ -226,7 +238,7 @@ Text("some view") ) ``` -## NDEF Tags +### NDEF Tags ```swift import TRETNFCKit_NDEFTag @@ -262,7 +274,7 @@ Text("some view") ) ``` -## NDEF Messages +### NDEF Messages ```swift import TRETNFCKit_NDEFMessage @@ -294,6 +306,159 @@ Text("some view") ) ``` +## Low-level wrappers + +### `AsyncNFCTagReaderSession` (for [`NFCTagReaderSession`](https://developer.apple.com/documentation/corenfc/nfctagreadersession)) +```swift +import TRETNFCKit_Async + +func asyncNFCTagReaderSessionSample() async { + guard AsyncNFCTagReaderSession.readingAvailable else { + return + } + + let readerSession = AsyncNFCTagReaderSession( + pollingOption: // ... + ) + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + // .. + case .sessionBecomeActive: + // .. + case .sessionDetected(let tags): + let tag = tags.first! + do { + try await readerSession.connect(to: tag) + // ... + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionCreationFailed(let reason): + // .. + case .sessionInvalidated(let reason): + // .. + } + } +} +``` + +#### with SwiftUI +See `Examples/TRETNFCKitExample/NFCNativeTagReaderExampleView.swift`. + +### `AsyncNFCNDEFTagReaderSession` (for [`NFCNDEFReaderSession`](https://developer.apple.com/documentation/corenfc/nfcndefreadersession) with [`NFCNDEFTag`](https://developer.apple.com/documentation/corenfc/nfcndefmessage)s) +```swift +import TRETNFCKit_Async + +func asyncNFCNDEFTagReaderSessionSample() async { + guard AsyncNFCNDEFTagReaderSession.readingAvailable else { + return + } + + let readerSession = AsyncNFCNDEFTagReaderSession( + invalidateAfterFirstRead: // ... + ) + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + // ... + case .sessionBecomeActive: + // ... + case .sessionDetected(let tags): + let tag = tags.first! + do { + try await readerSession.connect(to: tag) + // ... + readerSession.stop() + } catch { + readerSession.stop(errorMessage: error.localizedDescription) + } + case .sessionInvalidated(let reason): + // ... + } + } +} +``` + +#### with SwiftUI +See `Examples/TRETNFCKitExample/NFCNDEFTagReaderExampleView.swift`. + +### `AsyncNFCNDEFMessageReaderSession` (for [`NFCNDEFReaderSession`](https://developer.apple.com/documentation/corenfc/nfcndefreadersession) with [`NFCNDEFMessage`](https://developer.apple.com/documentation/corenfc/nfcndeftag)s) +```swift +import TRETNFCKit_Async + +func asyncNFCNDEFMessageReaderSessionSample() async { + guard AsyncNFCNDEFMessageReaderSession.readingAvailable else { + return + } + + let readerSession = AsyncNFCNDEFMessageReaderSession( + invalidateAfterFirstRead: // ... + ) + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + // ... + case .sessionBecomeActive: + // ... + case .sessionDetected(let messages): + // ... + readerSession.stop() + case .sessionInvalidated(let reason): + // ... + } + } +} +``` + +#### with SwiftUI +See `Examples/TRETNFCKitExample/NFCNDEFMessageReaderExampleView.swift`. + +### `AsyncNFCVASReaderSession` (for [`NFCVASReaderSession`](https://developer.apple.com/documentation/corenfc/nfcvasreadersession)) +```swift +import TRETNFCKit_Async + +func asyncNFCVASReaderSessionSample() async { + guard AsyncNFCVASReaderSession.readingAvailable else { + return + } + + let readerSession = AsyncNFCVASReaderSession( + vasCommandConfigurations: // ... + ) + + for await event in readerSession.eventStream { + switch event { + case .sessionIsReady: + readerSession.alertMessage = "Place the tag on a flat, non-metal surface and rest your iPhone on the tag." + readerSession.start() + case .sessionStarted: + // ... + case .sessionBecomeActive: + // ... + case .sessionReceived(let responses): + // ... + readerSession.stop() + case .sessionInvalidated(let reason): + // ... + } + } +} +``` + # Availability - iOS 13.0+ - iPadOS 13.0+ diff --git a/Sources/Async/AsyncNFCNDEFMessageReaderSession.swift b/Sources/Async/AsyncNFCNDEFMessageReaderSession.swift new file mode 100644 index 00000000..45bd80b9 --- /dev/null +++ b/Sources/Async/AsyncNFCNDEFMessageReaderSession.swift @@ -0,0 +1,112 @@ +// +// AsyncNFCNDEFMessageReaderSession.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +#if canImport(CoreNFC) +open class AsyncNFCNDEFMessageReaderSession: AsyncNFCReaderSession { + public init( + invalidateAfterFirstRead: Bool + ) { + (eventStream, eventStreamContinuation) = EventStream.makeStream() + bridge = .init(invalidateAfterFirstRead: invalidateAfterFirstRead) + bridge.sessionDidBecomeActive = { [unowned self] in + eventStreamContinuation.yield(.sessionBecomeActive) + } + bridge.sessionDidInvalidateWithError = { [unowned self] in + eventStreamContinuation.yield(.sessionInvalidated(reason: $0)) + eventStreamContinuation.finish() + } + bridge.sessionDidDetectNDEFs = { [unowned self] in + eventStreamContinuation.yield(.sessionDetected(messages: $0)) + } + eventStreamContinuation.yield(.sessionIsReady) + } + + public let eventStream: EventStream + + public var isReady: Bool { + bridge.isReady + } + + public var alertMessage: String { + get { bridge.alertMessage } + set { bridge.alertMessage = newValue } + } + + public func start() { + bridge.begin() + eventStreamContinuation.yield(.sessionStarted) + } + + public func stop() { + bridge.invalidate() + } + + private let bridge: NFCNDEFMessageReaderSessionBridge + private let eventStreamContinuation: EventStream.Continuation +} + +extension AsyncNFCNDEFMessageReaderSession { + public enum Event: AsyncNFCReaderSessionEvent { + case sessionIsReady + case sessionStarted + case sessionBecomeActive + case sessionDetected(messages: [NFCNDEFMessage]) + case sessionInvalidated(reason: NFCReaderError) + } +} + +extension AsyncNFCNDEFMessageReaderSession { + public typealias EventStream = AsyncStream +} + +private final class NFCNDEFMessageReaderSessionBridge: NSObject, NFCReaderSessionBridgeable, NFCNDEFReaderSessionDelegate { + private lazy var session: NFCNDEFReaderSession = { preconditionFailure("`session` has not been set.") }() + lazy var sessionDidBecomeActive: () -> Void = { preconditionFailure("`sessionDidBecomeActive` has not been set.") }() + lazy var sessionDidInvalidateWithError: (_ error: NFCReaderError) -> Void = { preconditionFailure("`sessionDidInvalidateWithError` has not been set.") }() + lazy var sessionDidDetectNDEFs: (_ messages: [NFCNDEFMessage]) -> Void = { preconditionFailure("`sessionDidDetectNDEFs` has not been set.") }() + + init( + invalidateAfterFirstRead: Bool + ) { + super.init() + session = .init( + delegate: self, + queue: nil, + invalidateAfterFirstRead: invalidateAfterFirstRead + ) + } + + var isReady: Bool { + session.isReady + } + + var alertMessage: String { + get { session.alertMessage } + set { session.alertMessage = newValue } + } + + func begin() { + session.begin() + } + + func invalidate() { + session.invalidate() + } + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { + sessionDidBecomeActive() + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: any Error) { + sessionDidInvalidateWithError(error as! NFCReaderError) + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + sessionDidDetectNDEFs(messages) + } +} +#endif diff --git a/Sources/Async/AsyncNFCNDEFTagReaderSession.swift b/Sources/Async/AsyncNFCNDEFTagReaderSession.swift new file mode 100644 index 00000000..38d06e34 --- /dev/null +++ b/Sources/Async/AsyncNFCNDEFTagReaderSession.swift @@ -0,0 +1,132 @@ +// +// AsyncNFCNDEFTagReaderSession.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +#if canImport(CoreNFC) +open class AsyncNFCNDEFTagReaderSession: AsyncNFCReaderSession { + public init( + invalidateAfterFirstRead: Bool + ) { + (eventStream, eventStreamContinuation) = EventStream.makeStream() + bridge = .init(invalidateAfterFirstRead: invalidateAfterFirstRead) + bridge.sessionDidBecomeActive = { [unowned self] in + eventStreamContinuation.yield(.sessionBecomeActive) + } + bridge.sessionDidInvalidateWithError = { [unowned self] in + eventStreamContinuation.yield(.sessionInvalidated(reason: $0)) + eventStreamContinuation.finish() + } + bridge.sessionDidDetect = { [unowned self] in + eventStreamContinuation.yield(.sessionDetected(tags: $0)) + } + eventStreamContinuation.yield(.sessionIsReady) + } + + public let eventStream: EventStream + + public var isReady: Bool { + bridge.isReady + } + + public var alertMessage: String { + get { bridge.alertMessage } + set { bridge.alertMessage = newValue } + } + + public func start() { + bridge.begin() + eventStreamContinuation.yield(.sessionStarted) + } + + public func connect(to tag: any NFCNDEFTag) async throws { + try await bridge.connect(to: tag) + } + + public func stop() { + bridge.invalidate() + } + + public func stop(errorMessage: String) { + bridge.invalidate(errorMessage: errorMessage) + } + + private let bridge: NFCNDEFTagReaderSessionBridge + private let eventStreamContinuation: EventStream.Continuation +} + +extension AsyncNFCNDEFTagReaderSession { + public enum Event: AsyncNFCReaderSessionEvent { + case sessionIsReady + case sessionStarted + case sessionBecomeActive + case sessionDetected(tags: [any NFCNDEFTag]) + case sessionInvalidated(reason: NFCReaderError) + } +} + +extension AsyncNFCNDEFTagReaderSession { + public typealias EventStream = AsyncStream +} + +private final class NFCNDEFTagReaderSessionBridge: NSObject, NFCReaderSessionBridgeable, NFCNDEFReaderSessionDelegate { + private lazy var session: NFCNDEFReaderSession = { preconditionFailure("`session` has not been set.") }() + lazy var sessionDidBecomeActive: () -> Void = { preconditionFailure("`sessionDidBecomeActive` has not been set.") }() + lazy var sessionDidInvalidateWithError: (_ error: NFCReaderError) -> Void = { preconditionFailure("`sessionDidInvalidateWithError` has not been set.") }() + lazy var sessionDidDetect: (_ tags: [any NFCNDEFTag]) -> Void = { preconditionFailure("`sessionDidDetect` has not been set.") }() + + init( + invalidateAfterFirstRead: Bool + ) { + super.init() + session = .init( + delegate: self, + queue: nil, + invalidateAfterFirstRead: invalidateAfterFirstRead + ) + } + + var isReady: Bool { + session.isReady + } + + var alertMessage: String { + get { session.alertMessage } + set { session.alertMessage = newValue } + } + + func begin() { + session.begin() + } + + func connect(to tag: any NFCNDEFTag) async throws { + try await session.connect(to: tag) + } + + func invalidate() { + session.invalidate() + } + + func invalidate(errorMessage: String) { + session.invalidate(errorMessage: errorMessage) + } + + func readerSessionDidBecomeActive(_ session: NFCNDEFReaderSession) { + sessionDidBecomeActive() + } + + func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: any Error) { + sessionDidInvalidateWithError(error as! NFCReaderError) + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) { + fatalError("The reader session doesn't call this method when the bridge provides the readerSession(_:didDetect:) method.") + } + + func readerSession(_ session: NFCNDEFReaderSession, didDetect tags: [any NFCNDEFTag]) { + sessionDidDetect(tags) + } +} +#endif diff --git a/Sources/Async/AsyncNFCReaderSession.swift b/Sources/Async/AsyncNFCReaderSession.swift new file mode 100644 index 00000000..1f4e3cd8 --- /dev/null +++ b/Sources/Async/AsyncNFCReaderSession.swift @@ -0,0 +1,22 @@ +// +// AsyncNFCReaderSession.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +public protocol AsyncNFCReaderSession: AnyObject { + associatedtype Event + associatedtype EventStream + var eventStream: EventStream { get } + var isReady: Bool { get } + var alertMessage: String { get set } + func start() + func stop() +} + +#if canImport(CoreNFC) +extension AsyncNFCReaderSession { + public static var readingAvailable: Bool { NFCReaderSession.readingAvailable } +} +#endif diff --git a/Sources/Async/AsyncNFCReaderSessionEvent.swift b/Sources/Async/AsyncNFCReaderSessionEvent.swift new file mode 100644 index 00000000..d138cd5d --- /dev/null +++ b/Sources/Async/AsyncNFCReaderSessionEvent.swift @@ -0,0 +1,14 @@ +// +// AsyncNFCReaderSessionEvent.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +public protocol AsyncNFCReaderSessionEvent { + associatedtype Error + static var sessionIsReady: Self { get } + static var sessionStarted: Self { get } + static var sessionBecomeActive: Self { get } + static func sessionInvalidated(reason: Error) -> Self +} diff --git a/Sources/Async/AsyncNFCTagReaderSession.swift b/Sources/Async/AsyncNFCTagReaderSession.swift new file mode 100644 index 00000000..cbd5ac2b --- /dev/null +++ b/Sources/Async/AsyncNFCTagReaderSession.swift @@ -0,0 +1,173 @@ +// +// AsyncNFCTagReaderSession.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +#if canImport(CoreNFC) +open class AsyncNFCTagReaderSession: AsyncNFCReaderSession { + public init( + pollingOption: NFCTagReaderSession.PollingOption + ) { + (eventStream, eventStreamContinuation) = EventStream.makeStream() + bridge = .init(pollingOption: pollingOption) + guard let bridge else { + eventStreamContinuation.yield( + .sessionCreationFailed( + reason: pollingOption.isEmpty ? .pollingOptionIsEmpty : .systemNotAvailable + ) + ) + eventStreamContinuation.finish() + return + } + bridge.sessionDidBecomeActive = { [unowned self] in + eventStreamContinuation.yield(.sessionBecomeActive) + } + bridge.sessionDidInvalidateWithError = { [unowned self] in + eventStreamContinuation.yield(.sessionInvalidated(reason: $0)) + eventStreamContinuation.finish() + } + bridge.sessionDidDetect = { [unowned self] in + eventStreamContinuation.yield(.sessionDetected(tags: $0)) + } + eventStreamContinuation.yield(.sessionIsReady) + } + + public let eventStream: EventStream + + public var isReady: Bool { + bridge?.isReady ?? false + } + + public var alertMessage: String { + get { bridge?.alertMessage ?? "" } + set { + guard let bridge = assertedBridge() else { return } + bridge.alertMessage = newValue + } + } + + public func start() { + guard let bridge = assertedBridge() else { return } + bridge.begin() + eventStreamContinuation.yield(.sessionStarted) + } + + public var connectedTag: NFCTag? { + guard let bridge = assertedBridge() else { return nil } + return bridge.connectedTag + } + + public func connect(to tag: NFCTag) async throws { + guard let bridge = assertedBridge() else { return } + try await bridge.connect(to: tag) + } + + public func stop() { + guard let bridge = assertedBridge() else { return } + bridge.invalidate() + } + + public func stop(errorMessage: String) { + guard let bridge = assertedBridge() else { return } + bridge.invalidate(errorMessage: errorMessage) + } + + private let eventStreamContinuation: EventStream.Continuation + private let bridge: NFCTagReaderSessionBridge? + + private func assertedBridge( + file: StaticString = #file, + line: UInt = #line + ) -> NFCTagReaderSessionBridge? { + guard let bridge else { + assertionFailure("Please check the `reason` from `.sessionCreationFailed(reason:)` sent to `eventStream`.", file: file, line: line) + return nil + } + return bridge + } +} + +extension AsyncNFCTagReaderSession { + public enum Event: AsyncNFCReaderSessionEvent { + public enum SessionCreationFailedReason: Sendable { + case pollingOptionIsEmpty + case systemNotAvailable + } + + case sessionIsReady + case sessionStarted + case sessionBecomeActive + case sessionDetected(tags: [NFCTag]) + case sessionCreationFailed(reason: SessionCreationFailedReason) + case sessionInvalidated(reason: NFCReaderError) + } +} + +extension AsyncNFCTagReaderSession { + public typealias EventStream = AsyncStream +} + +private final class NFCTagReaderSessionBridge: NSObject, NFCReaderSessionBridgeable, NFCTagReaderSessionDelegate { + private lazy var session: NFCTagReaderSession = { preconditionFailure("`session` has not been set.") }() + lazy var sessionDidBecomeActive: () -> Void = { preconditionFailure("`sessionDidBecomeActive` has not been set.") }() + lazy var sessionDidInvalidateWithError: (_ error: NFCReaderError) -> Void = { preconditionFailure("`sessionDidInvalidateWithError` has not been set.") }() + lazy var sessionDidDetect: (_ tags: [NFCTag]) -> Void = { preconditionFailure("`sessionDidDetect` has not been set.") }() + + init?( + pollingOption: NFCTagReaderSession.PollingOption + ) { + super.init() + guard let session = NFCTagReaderSession( + pollingOption: pollingOption, + delegate: self, + queue: nil + ) else { + return nil + } + self.session = session + } + + var isReady: Bool { + session.isReady + } + + var alertMessage: String { + get { session.alertMessage } + set { session.alertMessage = newValue } + } + + func begin() { + session.begin() + } + + var connectedTag: NFCTag? { + session.connectedTag + } + + func connect(to tag: NFCTag) async throws { + try await session.connect(to: tag) + } + + func invalidate() { + session.invalidate() + } + + func invalidate(errorMessage: String) { + session.invalidate(errorMessage: errorMessage) + } + + func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) { + sessionDidBecomeActive() + } + + func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: any Error) { + sessionDidInvalidateWithError(error as! NFCReaderError) + } + + func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + sessionDidDetect(tags) + } +} +#endif diff --git a/Sources/Async/AsyncNFCVASReaderSession.swift b/Sources/Async/AsyncNFCVASReaderSession.swift new file mode 100644 index 00000000..519576c3 --- /dev/null +++ b/Sources/Async/AsyncNFCVASReaderSession.swift @@ -0,0 +1,120 @@ +// +// AsyncNFCVASReaderSession.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +#if canImport(CoreNFC) +open class AsyncNFCVASReaderSession: AsyncNFCReaderSession { + public init( + vasCommandConfigurations: [NFCVASCommandConfiguration] + ) { + (eventStream, eventStreamContinuation) = EventStream.makeStream() + bridge = .init(vasCommandConfigurations: vasCommandConfigurations) + bridge.sessionDidBecomeActive = { [unowned self] in + eventStreamContinuation.yield(.sessionBecomeActive) + } + bridge.sessionDidInvalidateWithError = { [unowned self] in + eventStreamContinuation.yield(.sessionInvalidated(reason: $0)) + eventStreamContinuation.finish() + } + bridge.sessionDidReceive = { [unowned self] in + eventStreamContinuation.yield(.sessionReceived(responses: $0)) + } + eventStreamContinuation.yield(.sessionIsReady) + } + + public let eventStream: EventStream + + public var isReady: Bool { + bridge.isReady + } + + public var alertMessage: String { + get { bridge.alertMessage } + set { bridge.alertMessage = newValue } + } + + public func start() { + bridge.begin() + eventStreamContinuation.yield(.sessionStarted) + } + + public func stop() { + bridge.invalidate() + } + + public func stop(errorMessage: String) { + bridge.invalidate(errorMessage: errorMessage) + } + + private let bridge: NFCVASReaderSessionBridge + private let eventStreamContinuation: EventStream.Continuation +} + +extension AsyncNFCVASReaderSession { + public enum Event: AsyncNFCReaderSessionEvent { + case sessionIsReady + case sessionStarted + case sessionBecomeActive + case sessionReceived(responses: [NFCVASResponse]) + case sessionInvalidated(reason: NFCReaderError) + } +} + +extension AsyncNFCVASReaderSession { + public typealias EventStream = AsyncStream +} + +private final class NFCVASReaderSessionBridge: NSObject, NFCReaderSessionBridgeable, NFCVASReaderSessionDelegate { + private lazy var session: NFCVASReaderSession = { preconditionFailure("`session` has not been set.") }() + lazy var sessionDidBecomeActive: () -> Void = { preconditionFailure("`sessionDidBecomeActive` has not been set.") }() + lazy var sessionDidInvalidateWithError: (_ error: NFCReaderError) -> Void = { preconditionFailure("`sessionDidInvalidateWithError` has not been set.") }() + lazy var sessionDidReceive: (_ responses: [NFCVASResponse]) -> Void = { preconditionFailure("`sessionDidReceive` has not been set.") }() + + init( + vasCommandConfigurations: [NFCVASCommandConfiguration] + ) { + super.init() + session = .init( + vasCommandConfigurations: vasCommandConfigurations, + delegate: self, + queue: nil + ) + } + + var isReady: Bool { + session.isReady + } + + var alertMessage: String { + get { session.alertMessage } + set { session.alertMessage = newValue } + } + + func begin() { + session.begin() + } + + func invalidate() { + session.invalidate() + } + + func invalidate(errorMessage: String) { + session.invalidate(errorMessage: errorMessage) + } + + func readerSessionDidBecomeActive(_ session: NFCVASReaderSession) { + sessionDidBecomeActive() + } + + func readerSession(_ session: NFCVASReaderSession, didInvalidateWithError error: any Error) { + sessionDidInvalidateWithError(error as! NFCReaderError) + } + + func readerSession(_ session: NFCVASReaderSession, didReceive responses: [NFCVASResponse]) { + sessionDidReceive(responses) + } +} +#endif diff --git a/Sources/Async/NFCReaderSessionBridgeable.swift b/Sources/Async/NFCReaderSessionBridgeable.swift new file mode 100644 index 00000000..60a0fc68 --- /dev/null +++ b/Sources/Async/NFCReaderSessionBridgeable.swift @@ -0,0 +1,16 @@ +// +// NFCReaderSessionBridgeable.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +protocol NFCReaderSessionBridgeable: AnyObject { + associatedtype Error + var sessionDidBecomeActive: () -> Void { get set } + var sessionDidInvalidateWithError: (_ error: Error) -> Void { get set } + var isReady: Bool { get } + var alertMessage: String { get set } + func begin() + func invalidate() +} diff --git a/Sources/Async/_exported.swift b/Sources/Async/_exported.swift new file mode 100644 index 00000000..aa2c51a2 --- /dev/null +++ b/Sources/Async/_exported.swift @@ -0,0 +1,11 @@ +// +// _exported.swift +// Async +// +// Created by treastrain on 2024/01/28. +// + +@_exported import Foundation +#if canImport(CoreNFC) +@_exported import CoreNFC +#endif