Skip to content

Commit

Permalink
Merge pull request #1206 from treastrain/nfc-reader
Browse files Browse the repository at this point in the history
[iOS] Add retry support to `NFCReader`
  • Loading branch information
ry-itto authored Sep 13, 2023
2 parents 17091ba + b94b0f8 commit d12ebd0
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 40 deletions.
11 changes: 9 additions & 2 deletions app-ios/Modules/Sources/Achievements/AchievementsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Theme

public struct AchievementsView: View {
@ObservedObject var viewModel: AchievementsViewModel = .init()
@State private var isReadingNFCTag = false
@State private var obtainedAchievement: Achievement?

public init() {}
Expand Down Expand Up @@ -61,12 +62,18 @@ public struct AchievementsView: View {
}
Button {
Task {
let result = await viewModel.read()
isReadingNFCTag = true
let result = await viewModel.read(
didInvalidateHandler: {
isReadingNFCTag = false
}
)
obtainedAchievement = result
}
} label: {
Text("Scan NFC Tag")
Text(isReadingNFCTag ? "Scanning..." : "Scan the NFC Tag")
}
.disabled(isReadingNFCTag)
.buttonStyle(.bordered)
.controlSize(.large)
}
Expand Down
18 changes: 10 additions & 8 deletions app-ios/Modules/Sources/Achievements/AchievementsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,18 @@ class AchievementsViewModel: ObservableObject {
}
}

func read() async -> Achievement? {
func read(
didInvalidateHandler: @escaping () -> Void
) async -> Achievement? {
do {
let readed = try await nfcReader.read()
if
let urlString = readed,
let url = URL(string: urlString) {
if let dynamicLink = try await deepLink.dynamicLink(shortLink: url) {
return try await deepLink.handleDynamicLink(dynamicLink: dynamicLink)
let shortLink = try await nfcReader.read(
didInvalidateHandler: didInvalidateHandler,
urlResolver: { [weak deepLink] url in
await deepLink?.canBeResolved(from: url) ?? false
}
}
)
let dynamicLink = try await deepLink.dynamicLink(shortLink: shortLink)!
return try await deepLink.handleDynamicLink(dynamicLink: dynamicLink)
} catch {
print(error)
}
Expand Down
9 changes: 9 additions & 0 deletions app-ios/Modules/Sources/DeepLink/DeepLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ public class DeepLink {
DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url)
}

public func canBeResolved(from shortLink: URL) async -> Bool {
do {
let resolvedLink = try await resolveShortLink(url: shortLink)
return dynamicLink(customURL: resolvedLink) != nil
} catch {
return false
}
}

public func dynamicLink(shortLink: URL) async throws -> DynamicLink? {
let resolvedLink = try await resolveShortLink(url: shortLink)
return dynamicLink(customURL: resolvedLink)
Expand Down
117 changes: 87 additions & 30 deletions app-ios/Modules/Sources/NFC/NFCReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,111 @@ public enum NFCError: Error {
case unavailable
}

public class NFCReader: NSObject {
private var activeContinuation: CheckedContinuation<String?, any Error>?
private var session: NFCReaderSession?
public final class NFCReader: NSObject {
private var activeContinuation: CheckedContinuation<URL, any Error>?
private var session: NFCTagReaderSession?
private var didInvalidateHandler: (() -> Void)?
private var urlResolver: ((URL) async -> Bool)?
private var tagReaderSessionDidDetectTask: Task<(), Never>?

public override init() {}

public func read() async throws -> String? {
guard NFCNDEFReaderSession.readingAvailable else {
public func read(
didInvalidateHandler: @escaping () -> Void,
urlResolver: @escaping @Sendable (URL) async -> Bool
) async throws -> URL {
guard NFCTagReaderSession.readingAvailable,
let session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) else {
throw NFCError.unavailable
}
self.session = session
self.didInvalidateHandler = didInvalidateHandler
self.urlResolver = urlResolver

tagReaderSessionDidDetectTask?.cancel()
return try await withCheckedThrowingContinuation { continuation in
activeContinuation = continuation

session = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false)

session?.alertMessage = "Hold your iPhone near the item to learn more about it."

session?.begin()
self.session?.alertMessage = AlertMessage.scanning
self.session?.begin()
}
}

private func invalidateSession() {
session?.invalidate()
private func releaseSession() {
tagReaderSessionDidDetectTask?.cancel()
tagReaderSessionDidDetectTask = nil
session = nil
activeContinuation = nil
}

private func restartPolling(session: NFCTagReaderSession, alertMessage: String) {
session.alertMessage = alertMessage
DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
session.alertMessage = AlertMessage.scanning
session.restartPolling()
}
}
}

extension NFCReader: NFCNDEFReaderSessionDelegate {
public func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
if let message = messages.first, let record = message.records.first {
let result = switch String(data: record.type, encoding: .utf8) {
case "T":
record.wellKnownTypeTextPayload().0
case "U":
record.wellKnownTypeURIPayload()?.absoluteString
default:
record.wellKnownTypeTextPayload().0
extension NFCReader: NFCTagReaderSessionDelegate {
public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
// do nothing...
}

public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
activeContinuation?.resume(throwing: error)
releaseSession()
didInvalidateHandler?()
didInvalidateHandler = nil
}

public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
let ndefTags = tags.compactMap {
switch $0 {
case .feliCa(let tag as NFCNDEFTag), .iso7816(let tag as NFCNDEFTag), .iso15693(let tag as NFCNDEFTag), .miFare(let tag as NFCNDEFTag):
return ($0, tag)
@unknown default:
return nil
}
}

guard ndefTags.count < 2 else {
restartPolling(session: session, alertMessage: AlertMessage.moreThanOneTag)
return
}

activeContinuation?.resume(returning: result)
invalidateSession()
guard let (tag, ndefTag) = ndefTags.first else {
restartPolling(session: session, alertMessage: AlertMessage.notSupported)
return
}

tagReaderSessionDidDetectTask = Task {
do {
try await session.connect(to: tag)
let message = try await ndefTag.readNDEF()
let record = message.records.first
guard let url = record?.wellKnownTypeURIPayload() else {
restartPolling(session: session, alertMessage: AlertMessage.notSupported)
return
}
let success = await urlResolver?(url) ?? false
guard success else {
restartPolling(session: session, alertMessage: AlertMessage.notSupported)
return
}
session.alertMessage = AlertMessage.done
session.invalidate()
activeContinuation?.resume(returning: url)
releaseSession()
} catch {
restartPolling(session: session, alertMessage: AlertMessage.notSupported)
}
}
}
}

public func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
activeContinuation?.resume(throwing: error)
invalidateSession()
extension NFCReader {
private enum AlertMessage {
static let scanning = "Hold the top of your iPhone on the NFC tag and avoid metallic accessories."
static let moreThanOneTag = "More than 1 tag is detected, please remove all tags and try again."
static let notSupported = "The tag is not supported. Try again."
static let done = "Done!"
}
}

0 comments on commit d12ebd0

Please sign in to comment.