iOS平台下如何实现NFC读取结果的持久化展示直至用户主动关闭?
我太懂你这个糟心的问题了——iOS自带的NFC操作栏简直是个“不听话的小家伙”,不仅没法控制它显示多久,连什么时候消失都由系统说了算,想让用户好好看完那些复杂的读取结果真的费劲。你现在遇到的弹窗被压在操作栏后面的问题,其实是因为系统操作栏的消失有延迟,而你立刻弹出的alert刚好撞在了它的显示周期里。
下面给你几个实用的解决思路,亲测能改善体验:
思路一:延迟弹窗,避开系统操作栏的显示周期
最直接的办法就是等系统的操作栏完全消失后,再弹出你的自定义弹窗。只要给个足够稳妥的延迟时间,就能避免重叠的尴尬场景。修改你的代码如下:
tag.readNDEF { message, error in if let message = message { let textMessage = self.parsePayloads(payloads: message.records) ?? "(none)" session.alertMessage = "Field read: \(textMessage)" session.invalidate() // 延迟1.2秒再弹出弹窗,给系统操作栏足够的消失时间 DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { let alertView = UIAlertController(title: "Field Read", message: textMessage, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "OK", style: .default)) self.viewController?.present(alertView, animated: true) } } }
这里选1.2秒是因为不同iOS版本的操作栏消失时间略有差异,这个数值能覆盖大部分场景,既不会让用户等太久,也不会出现弹窗被遮挡的情况。
思路二:利用Session失效回调,精准把控弹窗时机
Core NFC的NFCNDEFReaderSession有个代理方法sessionDidInvalidate,这个方法会在会话失效(也就是你调用session.invalidate()之后)触发,此时系统操作栏已经进入消失流程。我们可以在这个方法里弹出弹窗,再配合短延迟,体验会更丝滑:
首先,先在你的类里加个属性存储读取结果:
private var lastNFCReadResult: String?
然后修改读取回调的代码:
tag.readNDEF { message, error in if let message = message { let textMessage = self.parsePayloads(payloads: message.records) ?? "(none)" session.alertMessage = "Field read: \(textMessage)" // 先把结果存起来 self.lastNFCReadResult = textMessage session.invalidate() } }
最后实现代理方法:
func sessionDidInvalidate(_ session: NFCNDEFReaderSession, error: Error?) { guard let textMessage = self.lastNFCReadResult else { return } // 延迟0.5秒弹窗,刚好赶上系统操作栏消失的节点 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { let alertView = UIAlertController(title: "Field Read", message: textMessage, preferredStyle: .alert) alertView.addAction(UIAlertAction(title: "OK", style: .default)) self.viewController?.present(alertView, animated: true) // 清空存储的结果,避免下次读取干扰 self.lastNFCReadResult = nil } }
这种方式比直接在读取回调里延迟更精准,因为它是基于会话失效的时机触发的,适配性更强。
思路三:自定义持久化展示界面(进阶方案)
如果想完全摆脱系统操作栏的限制,让展示风格和你的App统一,你可以在App界面里提前准备一个自定义的结果展示视图(比如半透明弹窗、底部固定浮层等),等系统操作栏消失后再显示它。比如:
- 先在你的ViewController里添加一个自定义的结果视图(初始设为隐藏):
private let resultView: UIView = { let view = UIView() view.backgroundColor = .white view.layer.cornerRadius = 12 view.clipsToBounds = true // 这里可以添加UILabel展示结果、关闭按钮等控件 let resultLabel = UILabel() resultLabel.numberOfLines = 0 resultLabel.textAlignment = .center view.addSubview(resultLabel) // 给label设置布局约束 resultLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ resultLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), resultLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), resultLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), resultLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60) ]) let closeBtn = UIButton(type: .system) closeBtn.setTitle("关闭", for: .normal) view.addSubview(closeBtn) closeBtn.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ closeBtn.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), closeBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor) ]) return view }()
- 在
viewDidLoad里把它添加到界面上并隐藏:
override func viewDidLoad() { super.viewDidLoad() view.addSubview(resultView) // 设置布局约束,让视图居中显示 resultView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ resultView.centerXAnchor.constraint(equalTo: view.centerXAnchor), resultView.centerYAnchor.constraint(equalTo: view.centerYAnchor), resultView.widthAnchor.constraint(equalToConstant: 280) ]) resultView.isHidden = true }
- 然后在
sessionDidInvalidate的延迟回调里,更新视图内容并显示:
func sessionDidInvalidate(_ session: NFCNDEFReaderSession, error: Error?) { guard let textMessage = self.lastNFCReadResult else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // 更新结果视图的文本内容 if let resultLabel = self.resultView.subviews.first(where: { $0 is UILabel }) as? UILabel { resultLabel.text = textMessage } // 给关闭按钮添加点击事件 if let closeBtn = self.resultView.subviews.first(where: { $0 is UIButton }) as? UIButton { closeBtn.addTarget(self, action: #selector(self.hideResultView), for: .touchUpInside) } // 显示视图 self.resultView.isHidden = false } } @objc private func hideResultView() { resultView.isHidden = true }
这种方式的好处是完全由你控制展示样式和时长,用户可以随时点击关闭,体验更连贯,也更符合App的整体设计风格。
总的来说,前两种方法是快速解决问题的方案,第三种是更定制化的进阶方案,你可以根据自己的需求选择~
备注:内容来源于stack exchange,提问作者davidgyoung




