Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Add retry support to NFCReader #1206

Merged
merged 3 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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!"
}
}
Loading