Руководство по CoreNFC

Туториалы

Руководство по CoreNFC

В этом туториале вы узнаете, как использовать CoreNFC для беспроводного подключения к другим устройствам или NFC-тегам.

Near Field Communication (NFC) - это технология для беспроводных устройств ближнего действия, позволяющая обмениваться данными с другими устройствами или инициировать действия на этих устройствах. Построенный с использованием радиочастотного поля, он позволяет устройствам, которые не имеют никакого питания, хранить небольшие фрагменты данных, а также позволяет другим устройствам с питанием считывать эти данные.

Устройства с iOS и watchOS уже несколько лет имеют встроенное в них оборудование NFC. Apple Pay, например, использует эту технологию для взаимодействия с платежными терминалами в магазинах. Однако разработчики не могли использовать NFC до iOS 11.

В iOS 13 Apple вновь укрепила свои позиции, представив Core NFC. С помощью этой технологии вы можете запрограммировать устройства iOS взаимодействовать с друг другом по-новому. Далее, вы узнаете, как можно использовать эту технологию.

Вы научитесь:

  • Записывать информацию в NFC-тег
  • Считывать эту информацию
  • Сохранять пользовательскую информацию в теге
  • Модифицировать данные, записанные в теге

Заметка

Чтобы выполнить все шаги, описанные в этом руководстве, вам понадобится следующее:

  • Устройство с iOS
  • Аккаунт Apple Developer
  • Оборудование NFC, на которое вы можете записывать и с которого вы сможете считывать информацию. Многие интернет-магазины предлагают NFC-теги по разумным ценам. Вы можете получить пакет NFC-тегов примерно за $10. Ищите с описанием, что он программируется, или с указанной емкостью хранения, обычно от 300 до 500 байт. Любое устройство с такой емкостью для начала работы с NFC подойдет.

Давайте приступим

Чтобы начать работу, загрузите проекты туториала с помощью кнопки Download Materials в верхней или нижней части этого руководства, и откройте начальный проект в папке starter. С помощью этого проекта вы узнаете:

  • Как установить NFC-тег в качестве местоположения
  • Как сканировать тег местоположения, чтобы увидеть его имя и журнал посещаемости
  • Как добавить посетителя в тег местоположения

Запустите, и вы увидите следующее:

Записываем на наш первый тег

Чтобы начать, выберите проект NeatoCache в разделе Project navigator. Затем, перейдите в Signing & Capability и выберите + Capability. Далее, из списка нам нужен Near Field Communication Tag Reading.

Это значит, что профиль вашего приложения настроен на использование NFC.

Затем, откройте Info.plist и добавьте следующее:

  • Key: Privacy – NFC Scan Usage Description
  • Value: Use NFC to Read and Write data

Эта запись нужна вам для того, чтобы сообщить пользователям, для чего вы используете функцию NFC, а также для того, чтобы соответствовать требованиям Apple, используя NFC в своем приложении.

Затем добавим функцию, которая может выполнять различные задачи NFC, обрабатываемые вашим приложением. Откройте NFCUtility.swift, добавьте следующий код и добавьте алиасы типов в верхнюю часть файла.

import CoreNFC

typealias NFCReadingCompletion = (Result<NFCNDEFMessage?, Error>) -> Void
typealias LocationReadingCompletion = (Result<Location, Error>) -> Void

Вам нужно импортировать CoreNFC для работы с NFC. Алиасы обеспечивают следующие функциональные возможности:

  • NFCReadingCompletion нужен для выполнения общих задач по чтению тегов
  • LocationReadingCompletion нужен для чтения тега, настроенного в качестве местоположения

Затем, добавьте следующие свойства в NFCUtility:

// 1
private var session: NFCNDEFReaderSession?
private var completion: LocationReadingCompletion?

// 2
static func performAction(
  _ action: NFCAction,
  completion: LocationReadingCompletion? = nil
) {
  // 3
  guard NFCNDEFReaderSession.readingAvailable else {
    completion?(.failure(NFCError.unavailable))
    print("NFC is not available on this device")
    return
  }

  shared.action = action
  shared.completion = completion
  // 4
  shared.session = NFCNDEFReaderSession(
    delegate: shared.self,
    queue: nil,
    invalidateAfterFirstRead: false)
  // 5
  shared.session?.alertMessage = action.alertMessage
  // 6
  shared.session?.begin()
}

Не волнуйтесь, если на этом шаге вы получите ошибку компиляции из-за несоответствия NFCNDEFReaderSessionDelegate, позже мы это исправим.

Вот то, что мы сделали:

  1. Добавили свойства session и completion для хранения активного сеанса чтения NFC и блока его завершения.
  2. Добавили статическую функцию в качестве точки входа для задач чтения и записи NFC. Вы будете использовать одноэлементный доступ к этой функции и к NFCUtility в целом.
  3. Убедились, что устройство поддерживает NFC.
  4. Создали NFCNDEFReaderSession, который представляет собой активное чтение сессии, а также установили делегата для уведомления о различных событиях сеанса чтения NFC.
  5. Установили свойство alertMessage в сеансе так, чтобы оно отображало текст пользователю внутри всплывающего окна NFC.
  6. Начали сеанс чтения. При вызове, всплывающее окно будет показывать пользователю любые инструкции, которые вы установите на предыдущем шаге.

Понятие NDEF

Обратите внимание, что приведенный выше код вводит еще одну аббревиатуру - NDEF, которая расшифровывается как NFC Data Exchange Format. Это стандартизированный формат для записи или чтения с устройства NFC. Рассмотрим две части NDEF, которые вы будете использовать:

  • NDEF Record содержит ваше значение полезной нагрузки, например строку, URL - адрес, пользовательские данные, длину и тип. Эта информация является NFCNDEFPayload в CoreNFC.
  • NDEF Message - это структура данных, содержащая записи NDEF. В сообщении NDEF может содержаться одна или несколько записей NDEF.

Обнаружение тегов

Теперь, когда вы настроили свой NFCReaderSession, пришло время настроить NFCUtility в качестве его делегата, чтобы вы могли получать уведомления о различных событиях, происходящих во время сеанса чтения.

Добавьте следующий код в нижнюю часть NFCUtility.swift:

// MARK: - NFC NDEF Reader Session Delegate
extension NFCUtility: NFCNDEFReaderSessionDelegate {
  func readerSession(
    _ session: NFCNDEFReaderSession,
    didDetectNDEFs messages: [NFCNDEFMessage]
  ) {
    // Not used
  }
}

Вскоре мы допишем это расширение, но обратите внимание, что в этом туториале вы ничего не будете делать с readerSession(_:didDetectNDEFs:). Мы добавляем его сюда для соответствия протоколу делегирования.

Чем больше вы будете взаимодействовать с технологией NFC, тем больше вы будете видеть вариантов возникновения ошибок на различных этапах процесса чтения и записи. Добавьте следующий код в ваше новое расширение для обработки ошибок:

private func handleError(_ error: Error) {
  session?.alertMessage = error.localizedDescription
  session?.invalidate()
}

Первая строка кода должна показаться вам знакомой. Она покажет сообщение об ошибке пользователю в модальном окне NFC. В случае ошибки вы также аннулируете сеанс, чтобы завершить его и позволить пользователю снова взаимодействовать с приложением.

Добавьте следующий код в расширение для обработки ошибок во время сеанса чтения NFC:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didInvalidateWithError error: Error
) {
  if let error = error as? NFCReaderError,
    error.code != .readerSessionInvalidationErrorFirstNDEFTagRead &&
      error.code != .readerSessionInvalidationErrorUserCanceled {
    completion?(.failure(NFCError.invalidated(message: 
      error.localizedDescription)))
  }

  self.session = nil
  completion = nil
}

Добавление этого метода делегата устранит все ошибки компиляции, с которыми вы сталкивались до этого момента.

Наконец, добавьте этот последний метод в свое расширение для того, чтобы разобраться с возможным обнаружением NFC-тегов:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didDetect tags: [NFCNDEFTag]
) {
  guard 
    let tag = tags.first,
    tags.count == 1 
    else {
      session.alertMessage = """
        There are too many tags present. Remove all and then try again.
        """
      DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) {
        session.restartPolling()
      }
      return
  }
}

Здесь реализуется метод, который вызовется при сканировании тега.

Думаю, что вы ожидаете наличие только одного тега, находящегося достаточно близко к телефону, но вы также должны учитывать вероятность появления нескольких тегов. Если приложение это обнаружит, то остановит сканирование и предупредит об этом пользователя. После показа сообщения сеанс чтения перезапустится и приложение позволит пользователю повторить попытку.

Настройка тега

Итак у вас один тег, и вы, вероятно, хотите с ним поработать. Добавьте следующий код после оператора guard в readerSession(_: didDetect:):

// 1
session.connect(to: tag) { error in
  if let error = error {
    self.handleError(error)
    return
  }

  // 2
  tag.queryNDEFStatus { status, _, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 3
    switch (status, self.action) {
    case (.notSupported, _):
      session.alertMessage = "Unsupported tag."
      session.invalidate()
    case (.readOnly, _):
      session.alertMessage = "Unable to write to tag."
      session.invalidate()
    case (.readWrite, .setupLocation(let locationName)):
      self.createLocation(name: locationName, with: tag)
    case (.readWrite, .readLocation):
      return
    default:
      return
    }
  }
}

В приведенном выше коде происходит следующее:

  1. Подключаетесь к обнаруженному тегу, используя текущий сеанс NCFNDEFReaderSession. Этот шаг нужен для того, чтобы выполнить любое чтение или запись в тег. После подключения, он будет вызывать свой обработчик завершения из-за любых возникающих ошибок.
  2. Запрашиваете у тега его статус NDEF, чтобы узнать, поддерживается ли устройство NFC. Статус должен быть readWrite для задач приложения NeatoCache.
  3. Переходите к статусу и действию NFC, и определяете, что следует сделать, основываясь на их значениях. Здесь вы пытаетесь настроить тег, чтобы иметь имя местоположения, используя createLocation(name:with:), еще не существующий, поэтому вы столкнетесь с ошибкой компиляции. Но не беспокойтесь, мы добавим его чуточку позже. Так же действие readLocation также будет реализовано далее.

Создание полезной нагрузки (payload)

До этого момента вы работали с поиском тега, подключением к нему и запросом его статуса. Чтобы завершить настройку записи в тег, добавьте следующий блок кода в конец NFCUtility.swift:

// MARK: - Utilities
extension NFCUtility {
  func createLocation(name: String, with tag: NFCNDEFTag) {
    // 1
    guard let payload = NFCNDEFPayload
      .wellKnownTypeTextPayload(string: name, locale: Locale.current) 
      else {
        handleError(NFCError.invalidated(message: "Could not create payload"))
        return
    }

    // 2
    let message = NFCNDEFMessage(records: [payload])

    // 3
    tag.writeNDEF(message) { error in
      if let error = error {
        self.handleError(error)
        return
      }

      self.session?.alertMessage = "Wrote location data."
      self.session?.invalidate()
      self.completion?(.success(Location(name: name)))
    }
  }
}

Вот, что вы делаете в приведенном выше коде:

  1. Создаете текстовый NFCNDEFPayload. Как уже говорилось ранее, это похоже на NDEF Record.
  2. Создаете новый NFCNDEFMessage с полезной нагрузкой, чтобы вы смогли сохранить его на устройстве NFC.
  3. Наконец, записываете сообщение в тег.

Использование типов NDEF Payload

NFCNDEFPayload поддерживает несколько различных типов данных. В этом примере вы используете wellKnownTypeTextPayload(string:locale:). Это довольно простой тип данных, который использует строку и локальное устройство. Некоторые другие типы данных содержат более сложную информацию. Вот полный список:

  • Empty
  • Well-Known
  • MIME media-type
  • Absolute URI
  • External
  • Unknown
  • Unchanged
  • Reserved

Заметка

Это руководство охватывает только Well-Known и Unknown.

Обратите внимание, что тип может иметь подтипы. Well-Known, например, имеет подтипы Text и URI.

Все, что осталось - это подключить ваш пользовательский интерфейс к вашему новому коду. Перейдите в раздел AdminView.swift и замените следующий код:

Button(action: {
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

на этот код:

Button(action: {
  NFCUtility.performAction(.setupLocation(locationName: self.locationName)) { _ in
    self.locationName = ""
  }
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

Это определит ваше местоположение с помощью текста, найденного в текстовом поле.

Запустите приложение, перейдите во вкладку Admin, введите имя и выберите пункт Save Location….

Вы увидите следующее:

Заметка

Помните, что вам нужны физическое устройство и NFC-тег, поддерживающий возможность записи.

Как только вы поместите свой телефон на NFC-тег, то увидите сообщение о том, что ваше местоположение было успешно сохранено.

Чтение NFC-тега

Отлично! Теперь, когда у вас есть приложение, которое может записывать строку в тег, вы должны обеспечить способность прочитать ее. Вернитесь к NFCUtility.swift и найдите следующий код в readerSession(_: didDetect:):

case (.readWrite, .readLocation):
  return

Затем, замените его на следующий:

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

Время для реализации метода readLocation(from:). Добавьте следующее в расширение Utilities, содержащее createLocation(name:with:):

func readLocation(from tag: NFCNDEFTag) {
  // 1
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }
    // 2
    guard 
      let message = message,
      let location = Location(message: message) 
      else {
        self.session?.alertMessage = "Could not read tag data."
        self.session?.invalidate()
        return
    }
    self.completion?(.success(location))
    self.session?.alertMessage = "Read tag."
    self.session?.invalidate()
  }
}

Это дополнение должно выглядеть знакомым вам, так как похоже на то, что вы ранее написали для тега.

  1. Во-первых, вы инициируете чтение тега. Если его можно прочитать, он будет возвращать любые сообщения, которые найдет.
  2. Затем вы попытаетесь создать Location из данных сообщения, если оно у вас есть. При этом используется пользовательский инициализатор, который принимает NFCNDEFMessage и извлекает из него имя. Если вам интересно, вы можете найти этот инициализатор в LocationModel.swift.

Наконец, откройте VisitorView.swift и в scanSection замените следующий код:

Button(action: {
}) {
  Text("Scan Location Tag…")
}

на этот:

Button(action: {
  NFCUtility.performAction(.readLocation) { location in
    self.locationModel = try? location.get()
  }
}) {
  Text("Scan Location Tag…")
}

Все готово для чтения вашего тега. Запускайте.

Во вкладке Visitors выберите Scan Location Tag…. вы увидите следующее:

Написание различных типов данных

Возможность записать строки полезная штука, но хочется записать и другие виды данных в теги.

Чтобы подготовиться к этому, добавьте следующее в NFCUtility.swift, в рамках расширения Utilities:

private func read(
  tag: NFCNDEFTag,
  alertMessage: String = "Tag Read",
  readCompletion: NFCReadingCompletion? = nil
) {
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 1
    if let readCompletion = readCompletion,
       let message = message {
      readCompletion(.success(message))
    } else if 
      let message = message,
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) {
      // 2
      self.completion?(.success(location))
      self.session?.alertMessage = alertMessage
      self.session?.invalidate()
    } else {
      self.session?.alertMessage = "Could not decode tag data."
      self.session?.invalidate()
    }
  }
}

Этот метод чтения тегов станет отправной точкой для большинства ваших действий с этого момента. Как вы можете видеть, он читает тег также, как и раньше. Однако, как только тег будет прочитан, он сделает одну из двух вещей:

  1. Вызовет обработчик завершения и передаст ему сообщение. Это будет полезно для связывания нескольких NFC задач вместе.
  2. Декодирует полезную нагрузку, чтобы вы могли анализировать записи тега. Давайте вернемся к этому несколько позже.

Написание пользовательских данных вместо строк

На этом этапе вы готовы преобразовать приложение из записи строк в тег в запись пользовательских данных в тег. Добавьте в расширение Utilities следующее:

private func createLocation(_ location: Location, tag: NFCNDEFTag) {
  read(tag: tag) { _ in
    self.updateLocation(location, tag: tag)
  }
}

Выше ваша новая функция для создания тега с местоположением. Вы можете видеть, что она использует read(tag:alsertMessage:readCompletion:) для запуска процесса и вызывает новую функцию для обновления местоположения на теге, а также новый метод updateLocation(_:tag:).

Поскольку вы заменяете способ записи информации о местоположении в тег, удалите createLocation(name:with:) в начале расширения NFCUtility, так как оно больше не требуется. Также, обновите свой код в readerSession(_: didDetect:), заменяя этот код:

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(name: locationName, with: tag)

следующим:

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(Location(name: locationName), tag: tag)

Затем добавьте следующий блок кода после createLocation(_:tag:):


private func updateLocation(
  _ location: Location,
  withVisitor visitor: Visitor? = nil,
  tag: NFCNDEFTag
) {
  // 1
  var alertMessage = "Successfully setup location."
  var tempLocation = location
  
  // 2
  let jsonEncoder = JSONEncoder()
  guard let customData = try? jsonEncoder.encode(tempLocation) else {
    self.handleError(NFCError.invalidated(message: "Bad data"))
    return
  }
  // 3
  let payload = NFCNDEFPayload(
    format: .unknown,
    type: Data(),
    identifier: Data(),
    payload: customData)
  // 4
  let message = NFCNDEFMessage(records: [payload])
}

Рассмотрим, что вы делаете в приведенном выше коде:

  1. Создаете дефолтное оповещение об ошибке и временное местоположение. Вы вернетесь к ним позже.
  2. Закодируете структуру Location, переданную в функцию. Это приведет к преобразованию модели в необработанную Data. Это важно, так как именно так вы записываете любой пользовательский тип в NFC-тег.
  3. Создаете полезную нагрузку (payload), которая может обрабатывать ваши данные. Однако, unknown используется вами как формат. При этом вы должны установить type и identifier для пустой Data, в то время как аргумент payload содержит фактическую декодированную модель.
  4. Добавляете полезную нагрузку во вновь созданное сообщение.

В целом, полученное не сильно отличается от сохранения строки в теге. Вы просто добавляете дополнительный шаг для преобразования типа данных Swift в то, что понимает тег.

Проверка емкости тега

Чтобы закончить запись данных в тег, добавьте следующий блок кода в updateLocation(_:withVisitor:tag):

tag.queryNDEFStatus { _, capacity, _ in
  // 1
  guard message.length <= capacity else {
    self.handleError(NFCError.invalidPayloadSize)
    return
  }

  // 2
  tag.writeNDEF(message) { error in
    if let error = error {
      self.handleError(error)
      return
    }
    
    if self.completion != nil {
      self.read(tag: tag, alertMessage: alertMessage)
    }
  }
}

Приведенное выше замыкание пытается запросить текущий статус NDEF, а затем:

  1. Убеждается, что устройство имеет достаточно места для хранения местоположения. Помните, что NFC-теги часто имеют чрезвычайно ограниченный объем памяти по сравнению с устройствами, с которыми вы, возможно, знакомы.
  2. Записывает сообщение в тег.

Запустите и настройте местоположение, как вы сделали выше. Вы можете использовать тот же тег, что и раньше, если хотите, так как ваш новый код перезапишет все ранее сохраненные данные.

Чтение вашей пользовательской базы данных

На этом этапе, если вы попытаетесь прочитать свой тег, то получите ошибку. Сохраненные данные больше не являются типом well-known. Чтобы исправить это, замените следующий код в readerSession(_:didDetect:):

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

этим:

case (.readWrite, .readLocation):
  self.read(tag: tag)

Запустите и просканируйте тег. Поскольку вы вызываете read(tag:alertMessage:readCompletion:) без каких-либо завершающих обработчиков, он будет декодировать данные, найденные в первой записи сообщения.

Изменение контента

Последнее требование к этому приложению - сохранение журнала посетителей. В вашем приложении уже есть неиспользуемая функция, которая позволяет пользователям вводить свое имя и добавлять его в тег. Работа, которую вы проделали до этого момента, сделает остальную часть установки тривиальной задачей. Вы уже можете читать и записывать данные в тег, поэтому его изменение не должно составить труда.

Находясь в NFCUtility.swift, добавьте этот код в updateLocation(_: withVisitor:tag:) сразу после создания tempLocation:

if let visitor = visitor {
  tempLocation.visitors.append(visitor)
  alertMessage = "Successfully added visitor."
}

В приведенном выше коде вы проверяете, было ли предоставлено значение для visitor. Если это так, вы добавляете его в массив locations visitors.

Затем, добавьте следующее в расширение Utilities:

private func addVisitor(_ visitor: Visitor, tag: NFCNDEFTag) {
  read(tag: tag) { message in
    guard 
      let message = try? message.get(),
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) 
      else {
        return
    }

    self.updateLocation(location, withVisitor: visitor, tag: tag)
  }
}

Этот метод будет читать тег, получать от него сообщение и пытаться расшифровать Location на нем.

Далее, в readerSession(_:didDetect:), добавьте этот код в оператор switch:

case (.readWrite, .addVisitor(let visitorName)):
  self.addVisitor(Visitor(name: visitorName), tag: tag)

Если пользователь захочет добавить visitor, вы вызовете функцию, добавленную на предыдущем шаге.

Все, что остается - это обновить VisitorView.swift. В разделе visitorSection замените следующий код:

Button(action: {
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

следующим:

Button(action: {
  NFCUtility
    .performAction(.addVisitor(visitorName: self.visitorName)) { location in
      self.locationModel = try? location.get()
      self.visitorName = ""
    }
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

Запустите приложение, а затем перейдите во вкладку Visitors. Введите свое имя, затем выберите Add To Tag.... После сканирования вы увидите обновленное местоположение со списком посетителей (visitors), найденных по тегу.

Скачать проект

Оригинал статьи

Комментарии

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: