首页 iOS13 NFC 读写开发
文章
取消

iOS13 NFC 读写开发

iOS13 发布以后,苹果开放了NFC 写入的权限,相比起原来只能读取的半残状况,现在可以做的事情就更多了,刚好最近完成了一个NFC 读写相关的小应用,中间踩了不少坑,在此记录一下。值得注意的是虽然现在苹果在iOS13以后提供了写入的接口,但是仍然有不少限制。

首先是硬件方面的限制,只有iPhone7 以后的设备(包括iPhone7)才具有写入的功能,如果你查看苹果官网的设备信息会看到iPhon7 以后搭载的是“支持读卡器模式的 NFC”,而之前的iPhone 则是 “NFC”,换言之,只有iPhone7以后的设备才具有读写功能,并且目前还不支持卡模拟,而iPhone7之前的设备只有绑定银行卡付款的功能,连读取 NFC标签都做不到,实在是非常的残念….

除了这些,苹果支持的 NFC 芯片类型也有限,官网原话:

Your app can read tags to give users more information about their physical environment and the real-world objects in it. Using Core NFC, you can read Near Field Communication (NFC) tags of types 1 through 5 that contain data in the NFC Data Exchange Format (NDEF). For example, your app might give users information about products they find in a store or exhibits they visit in a museum.

Your app can also write data to tags, and interact with protocol specific tag such as ISO 7816, ISO 15693, FeliCa™, and MIFARE® tags.

我开发时使用的芯片有两种,一种是NDEF,还有种是 ISO 15693,所以接下来的例子主要围绕这两种芯片进行。

附上一张iPhone NFC设备支持表:


读取与写入

首先我们开始创建工程:

  • 编译器:Xcode11

  • macOS:10.15.1

创建完成后在Signing & Capabilities 里面点击“+”号添加Near Field Communication Tag Reading

添加后系统会自动帮你生成.entitlements 的环境文件,在这里其实有个大坑,创建完成后自带有NEDF 的标签,我做的应用还需要支持ISO 15693,根据官方文档添加以后,文件是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
 <key>com.apple.developer.nfc.readersession.formats</key>
 <array>
  <string>NDEF</string> 
  <string>TAG</string> //需支持ISO 15693, 自己添加
 </array>
</dict>
</plist>

这个环境文件是必须的,否则 NFC不会读取任何标签,但是这个配置有坑,一直到自己交付上传应用商店以前没有任何问题,但是最后上传到苹果应用商店时会报错

1
ERROR ITMS-90778:"Invalid entitlement for core nfc framework....because 'NDEF is disallowed'"

emm…..非常懵逼,查询资料后知道解决方式是删除上面文件中的<string>NDEF</string>字段,只保留<string>TAG</string> ,个人感觉应该是苹果后期把 NFC标签做了整合,或者是NDEF 和 TAG 有相互干扰的情况,所以会出现这个错误,在解决的时候还遇到了一个小坑,有人说在Signing & Capabilities中删除Near Field Communication Tag Reading 标签然后再重新添加可以解决,本人也试过,但还是报这个错,最后发现原因是删除后再添加Near Field Communication Tag Reading,系统会自动创建.entitlements文件,但是这个文件可能不会自动帮你引入到工程中,而是存在于项目的根目录中,该文件创建后仍然包含<string>NDEF</string>,所以上传会报同样的错误…

NDEF

NDEF 标签的读取与写入苹果官网有很详细的例子Demo,Demo源码:下载

由于自己做的项目需要自动识别 NDEF标签与ISO 15693 标签,所以并没有使用官网的方法,如果只需要读写NDEF的标签,查看官网的demo就足够了

ISO 15693

想要对NFC 标签进行操作,我们首先需要先建立一个 会话,点击按钮以后弹出 NFC交互界面

1
2
3
4
5
6
7
8
9
10
11
12
13
func callNFC(workMode: NFCWorkModel) {
       //判断设备是否能扫描标签
        guard NFCTagReaderSession.readingAvailable else {
            SVProgressHUD.showError(withStatus: NSLocalizedString("scanError", comment: ""))
            return
        }

        tagSession = NFCTagReaderSession(pollingOption: NFCTagReaderSession.PollingOption.iso15693,
                                         delegate: self, // 实现NFCTagReaderSessionDelegate
                                         queue: nil)
        tagSession?.alertMessage = NSLocalizedString("scanTip", comment: "")
        tagSession?.begin()
    }

建立会话时要求实现NFCTagReaderSessionDelegate 代理方法,主要用到的方法有两个,会话消失时会调用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  // nfc 会话取消,交互界面消失会走该方法
    func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) {
        // Check the invalidation reason from the returned error.
        if let readerError = error as? NFCReaderError {
            // Show an alert when the invalidation reason is not because of a
            // successful read during a single-tag read session, or because the
            // user canceled a multiple-tag read session from the UI or
            // programmatically using the invalidate method call.
            if readerError.code != .readerSessionInvalidationErrorFirstNDEFTagRead,
                readerError.code != .readerSessionInvalidationErrorUserCanceled {
                DispatchQueue.main.async {
                    SVProgressHUD.showError(withStatus: error.localizedDescription)
                }
            }
        }

        // To read new tags, a new session instance is required.
        //释放资源
        tagSession = nil
    }

获取到 NFC标签信息会调用该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
     //判断标签个数是否为多个
        if tags.count > 1 {
            let retryInterval = DispatchTimeInterval.milliseconds(500)
            session.invalidate(errorMessage: NSLocalizedString("manyTagTip", comment: ""))
            DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) {
                session.restartPolling()
            }
            return
        }

     //监测是否为ISO 15693标签
        guard case let NFCTag.iso15693(resultTag) = tags.first! else {
            return
        }

     //通过会话与NFC 芯片建立连接
        session.connect(to: tags.first!) { [weak self] (error: Error?) in

             //建立连接错误
            if error != nil {
                DDLogError("error-->\(String(describing: error?.localizedDescription))")
                session.invalidate(errorMessage: NSLocalizedString("connectError", comment: ""))
                session.invalidate()
                return
            }

            //先默认读取的芯片为NDEF 芯片,查询NDEF 芯片状态
            resultTag.queryNDEFStatus { (ndefStatus: NFCNDEFStatus, _: Int, error: Error?) in

                let retryInterval = DispatchTimeInterval.milliseconds(200)
                if ndefStatus == .notSupported { //ndef状态不支持, 连接芯片为ISO 15693芯片

                  //业务逻辑
                  
                  //读取数据
                  // self?.iso15693Tag(s1Tag: resultTag, session: session)
                  
                  //或者写入数据
                  //DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) {
                    // 写数据
                    //self?.writeISO5693Data(resultTag: resultTag, session: session)
                  //}

                    return

                } else if error != nil {
                    session.invalidate(errorMessage: error!.localizedDescription)
                    session.invalidate()
                    return
                }

                // NDEF 标签业务逻辑
               
                // 读数据
                // self?.ndefTag(ndefTag: resultTag, session: session)
                    return
                
        // 写数据
               // DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval) {
                //    self?.writeNdefData(resultTag: resultTag, 
                //                         session: session,
                //                       ndefStatus: ndefStatus) // ndef芯片
                //}
            }
        }
    }

读取数据方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 func ndefTag(ndefTag: NFCISO15693Tag, session: NFCTagReaderSession) {
        // 获取UID
        sendUID(resultTag: ndefTag, session: session)
        ndefTag.readNDEF { message, _ in
            guard let payload = message?.records.last else {
                DDLogError("message:\(String(describing: message))")
                session.invalidate(errorMessage: "读取错误")
                session.invalidate()
                return
            }

            let touple = payload.wellKnownTypeTextPayload()
            if let ndefStr = touple.0 {
                DDLogDebug("nedfstr--->\(ndefStr)")
                //ndefStr为获取到的标签数据
               //...业务逻辑
              
                session.alertMessage = NSLocalizedString("readSuccess", comment: "")
                session.invalidate()
                return
            }

            session.invalidate(errorMessage: "解析错误")
            session.invalidate()
        }
 }

 func iso15693Tag(s1Tag: NFCISO15693Tag, session: NFCTagReaderSession) {
        // 获取UID
        sendUID(resultTag: s1Tag, session: session)
    
      //发送读取命令,读取的block 地址为0xF8
        s1Tag.readSingleBlock(requestFlags: [.highDataRate, .address],
                                blockNumber: 0xF8) { data, error in

            if error != nil {
                DDLogError("error==>\(String(describing: error?.localizedDescription))")
                session.invalidate(errorMessage:"读取错误")
                session.invalidate()
                return
            }
            let dataStr = Util.bytesToStr(bytes: [UInt8](data))
            DDLogDebug("dataStr--->\(String(describing: dataStr))")
            //dataStr 为[UInt8] 解析的字符串
      //....业务逻辑
                                                    
            session.alertMessage = NSLocalizedString("readSuccess", comment: "")
            session.invalidate()
        }
    }

写入数据方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 // ndef 芯片写入数据
    private func writeNdefData(resultTag: NFCISO15693Tag, session: NFCTagReaderSession, ndefStatus: NFCNDEFStatus) {
     
       // command:自己拼接的写入信息,NDEF芯片可以写入多种信息,详情见官网,该处仅写入字符串
        let textPayload = NFCNDEFPayload.wellKnownTypeTextPayload(string: command,
                                                                  locale: Locale(identifier: "en"))

        let message = NFCNDEFMessage(records: [textPayload!])

        if ndefStatus == .readOnly {
            session.invalidate(errorMessage: "标签为只读状态")
        } else if ndefStatus == .readWrite {
            // When a tag is read-writable and has sufficient capacity,
            // write an NDEF message to it.
            resultTag.writeNDEF(message) { (error: Error?) in
                if error != nil {
                    session.invalidate(errorMessage: "写入失败")
                } else {
                   //写入成功
                    // 获取UID
                    self.sendUID(resultTag: resultTag, session: session)
                    session.alertMessage = NSLocalizedString("writeSuccess", comment: "")
                    session.invalidate()
                }
            }
        } else {
            session.invalidate(errorMessage:"标签状态错误")
        }
    }

 private func writeISO5693Data(resultTag: NFCISO15693Tag, session: NFCTagReaderSession) {
       
      //command:根据业务需要自己拼接的16进制命令数组
      //blockNumber: 写入地址
        let writeData = command.map { UInt8($0) }
        resultTag.writeSingleBlock(requestFlags: [.highDataRate, .address],
                                   blockNumber: 0xF8,
                                   dataBlock: Data(writeData)) { error in
            if error != nil {
                DDLogError("error==>\(String(describing: error?.localizedDescription))")
                session.invalidate(errorMessage: "写入失败")
                session.invalidate()
                return
            }
      //写入成功
            // 获取UID
            self.sendUID(resultTag: resultTag, session: session)
            session.invalidate()
        }
        return
    }

相关的工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private func sendUID(resultTag: NFCISO15693Tag, session _: NFCTagReaderSession) {
        let tagUID: [UInt8] = [UInt8](resultTag.identifier)
        let identifier = Util.bytesToStr(bytes: tagUID)
        readUIDSubject.onNext(identifier)
}

static func bytesToStr(bytes: [UInt8]) -> String {
        var hexStr = ""
        for index in 0 ..< bytes.count {
            var str = bytes[index].description
            if str.count == 1 {
                str = "0" + str
            } else {
                let low = Int(str)! % 16
                let hight = Int(str)! / 16
                str = hexIntToStr(hexInt: hight) + hexIntToStr(hexInt: low)
            }
            hexStr += str
        }
        return hexStr
 }

至此,iOS13 NFC的读取与写入就可以告一段落了,由于本人只涉及了两种芯片的开发,在其它方面肯定有所遗漏,如有错误或者更好的实现方法,欢迎来信讨论。

by:在开发期间苹果的官方文档给了自己很大的帮助:传送门

本文由作者按照 CC BY 4.0 进行授权