Как работать с DataScannerViewController в SwiftUI
На WWDC22 Apple представила iOS и iPadOS разработчикам замечательные инструменты сканирования данных на основе Live Text, которые позволяют пользователям сканировать текст и QR-коды с помощью камеры, аналогично интерфейсу Live Text в приложении Camera.
В этой статье я сосредоточусь на новом API под названием DataScannerViewController и поделюсь своим опытом встраивания этого API в существующий код SwiftUI. На фото ниже вы можете увидеть, как будет выглядеть сегодняшнее демонстрационное приложение.
Сперва нужно указать причину использования камеры
Поскольку это демонстрационное приложение может работать только на реальных устройствах с камерами, вам нужно, чтобы пользователи дали вам к ним доступ. Для этого вам нужно предоставить им четкое логичное объяснение того, зачем вам нужен доступ к их камерам.
Причину, по которой вы хотите использовать их камеры, вы можете указать в конфигурации проекта Xcode. Для этого вам нужно добавить ключ NSCameraUsageDescription в Information Property List таргета в Xcode.
Следующие шаги, которые я здесь привожу, взяты из официальной документации Apple “Сканирование данных камерой”:
В редакторе проекта выберите таргет и нажмите Info.
В любой строчке раздела “Custom iOS Target Properties” нажмите кнопку “Plus”.
Во всплывающем меню в колонке “Key” выберите “Privacy — Camera Usage Description”.
В колонке “Value” введите вашу причину, например, “Ваша камера нужна для сканирования текста и QR-кодов”.
Создание главного представления
Добавьте следующий код в ваш файл ContentView.swift
:
VStack {
Text(scanResults)
.padding()
Button {
// Для запуска сканирования документа
} label: {
Text("Tap to Scan Documents")
.foregroundColor(.white)
.frame(width: 300, height: 50)
.background(Color.blue)
.cornerRadius(10)
}
}
Где scanResult
– это переменная типа String, представляющая результат сканирования камеры, которая будет использоваться для иллюстрации того, что камера видит во время сканирования.
@State private var scanResults: String = ""
Кнопка здесь используется для презентации представления сканирования (scanning view).
Когда кто-то нажмет эту кнопку, устройство перейдет в режим сканирования данных. Однако не все устройства поддерживают эту фичу. Или даже если устройство поддерживает сканирование данных, но пользователь отказывает в предоставлении разрешения на использование камеры, то при нажатии на кнопку может произойти сбой приложения.
Для таких случаев я добавляю представление с предупреждением (alert view), которое отображает сообщение, объясняющее, что устройство по каким-либо причинам не может сканировать данные.
@State private var showDeviceNotCapacityAlert = false
Приведенный выше код определяет переменную для выбора, показывать ли представление с предупреждением или нет. Если showDeviceNotCapacityAlert
является true
, то пользователь вместо представления сканирования увидит представление с предупреждением.
Добавьте следующий код после кода с VStack
, который мы определили выше:
.alert("Scanner Unavailable", isPresented: $showDeviceNotCapacityAlert, actions: {})
Наконец, когда устройство готово к сканированию данных, нам нужно презентовать представление сканирования подобно приведенному выше коду. Добавьте следующий код в ваш файл ContentView.swift
:
@State private var showCameraScannerView = false
var body: some View {
VStack {
...
}
.sheet(isPresented: $showCameraScannerView) {
// Презентация представления сканирования
}
...
}
Нам осталось позаботиться о том, чтобы при нажатии на кнопку отображалось предупреждение, если устройство не способно сканировать данные. Чтобы проверить, может ли устройство использовать эту фичу, мы будем использовать переменную isDeviceCapacity
.
@State private var isDeviceCapacity = false
Теперь добавьте следующий код внутрь экшена кнопки:
if isDeviceCapacity {
self.showCameraScannerView = true
} else {
self.showDeviceNotCapacityAlert = true
}
Создание представления сканера камеры
Создайте новый swift-файл и назовите его CameraScanner.swift
. Добавьте туда следующий код:
struct CameraScanner: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
Text("Scanning View")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
self.presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
}
}
.interactiveDismissDisabled(true)
}
}
}
Обработка ситуаций, когда сканер недоступен
Когда приложение открывается пользователями, с самого начала нам нужно проверить, доступен ли сканер. Просто добавьте следующий код сразу после открытия приложения:
var body: some View {
VStack {
...
}
.onAppear {
isDeviceCapacity = (DataScannerViewController.isSupported && DataScannerViewController.isAvailable)
}
...
}
Только если вышеуказанное свойство, которое проверяет оба значения, возвращает true, мы можем открыть наше представление сканирования.
Создание контроллера представления сканера данных
Чтобы реализовать контроллер представления (view controller), который можно использовать в представлении SwiftUI, прежде всего нам нужно обернуть UIKit контроллер представления в UIViewControllerRepresentable
. Создайте новый swift-файл с именем CameraScannerViewController.swift
и просто добавьте туда следующий код:
import SwiftUI
import UIKit
import VisionKit
struct CameraScannerViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> DataScannerViewController {
let viewController = DataScannerViewController(recognizedDataTypes: [.text()],qualityLevel: .fast,recognizesMultipleItems: false, isHighFrameRateTrackingEnabled: false, isHighlightingEnabled: true)
return viewController
}
func updateUIViewController(_ viewController: DataScannerViewController, context: Context) {}
}
Вышеприведенный код возвращает контроллер представления, который предоставляет интерфейс для сканирования элементов в лайв-видео. В этой статье я сосредоточусь только на сканировании текстовых данных, поэтому recognizedDataTypes
здесь содержится только свойство .text()
.
Протокол Handle Delegate
После создания контроллера представления и до того, как мы его презентуем, нам нужно установить его делегат на объект в этом приложении, который обрабатывает колбеки протокола DataScannerViewControllerDelegate.
В UIKit можно просто написать следующий код:
viewController.delegate = self
К счастью, в SwiftUI обрабатывать DataScannerViewControllerDelegate очень удобно.
Координаторы SwiftUI предназначены для работы в качестве делегатов для контроллеров представления UIKit. На всякий случай напомню, что “делегаты” — это объекты, которые реагируют на события, происходящие в другом месте. Например, UIKit позволяет нам прикрепить объект-делегат к его представлению текстового поля, и этот делегат будет уведомлен, когда пользователь что-либо вводит, когда он нажимает клавишу ввода и так далее. Это означает, что разработчики UIKit могут изменять поведение своего текстового поля без необходимости создавать собственный кастомный тип текстового поля.
Так что добавим следующий код внутрь CameraScannerViewController
:
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, DataScannerViewControllerDelegate {
var parent: CameraScannerViewController
init(_ parent: CameraScannerViewController) {
self.parent = parent
}
}
Мы также можем использовать аналогичный код внутри makeUIViewController
прямо над return:
func makeUIViewController(context: Context) -> DataScannerViewController {
...
viewController.delegate = context.coordinator
return viewController
}
Начинаем сканировать данные
Наконец пришло время начать сканирование данных. Как только пользователь разрешит доступ к камере, вы можете начинать поиск элементов, которые появляются в видео в реальном времени, вызвав метод startScanning(). В этом случае, когда представление сканирования уже презентовано, нам понадобится отдельное представление для выполнения действия сканирования.
Для того, чтобы дать представлению сканирования команду сканировать, нам понадобиться еще одна переменная. Добавьте следующий код в CameraScannerViewController
:
@Binding var startScanning: Bool
Когда значение startScanning
установлено в true, нам нужно обновить UIViewController и начать сканирование. Добавьте следующий код внутрь updateUIViewController
:
func updateUIViewController(_ viewController: DataScannerViewController, context: Context) {
if startScanning {
try? viewController.startScanning()
} else {
viewController.stopScanning()
}
}
Реакция на тап по элементу
Когда мы касаемся распознанного в лайв-видео элемента, контроллер представления вызывает метод-делегат dataScanner(:didTapOn:) и передает ему распознанный элемент. Вы можете реализовать этот метод, чтобы выполнять какие-либо действия в зависимости от элемента, к которому прикасается пользователь. Используйте параметры перечисления RecognizedItem для получения подробной информации об элементе (например, его границы или содержимое).
В нашем случае, чтобы обрабатывать тап по тексту, распознанному камерой, мы реализуем метод dataScanner(:didTapOn:), который будет выполнять экшн, отображающий результат на экране. Добавим следующий код внутрь класса Coordinator
:
class Coordinator: NSObject, DataScannerViewControllerDelegate {
...
func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
switch item {
case .text(let text):
parent.scanResult = text.transcript
default:
break
}
}
}
И добавьте Binding
свойство внутрь CameraScannerViewController
:
@Binding var scanResult: String
Сканируем
Пришло время обновить файл CameraScanner.swift
. Просто добавьте следующий код:
@Binding var startScanning: Bool
@Binding var scanResult: String
И измените Text(“Scanning View”)
на следующий код:
var body: some View {
NavigationView {
CameraScannerViewController(startScanning: $startScanning, scanResult: $scanResult)
...
}
}
Наконец, добавьте код, приведенный ниже в ContentView
:
struct ContentView: View {
...
@State private var scanResults: String = ""
var body: some View {
VStack {
...
}
.sheet(isPresented: $showCameraScannerView) {
CameraScanner(startScanning: $showCameraScannerView, scanResult: $scanResults)
}
...
}
}
scanResults
используется для передачи значения между представлениями. Как только камера что-то просканирует, scanResults
будет обновлен, а вслед за ними будет обновлено текстовое представление (text view).
Теперь запустите этот проект и наслаждайтесь.
Что дальше?
WWDC не только добавляет Live Text в камеру, но и упрощает разработчикам возможность взаимодействия Live Text с изображениями. Если вам интересно, как добавить взаимодействие Live Text с изображениями, ознакомьтесь с моей новой статьей: WWDC22: Добавляем взаимодействие Live Text с изображениями в SwiftUI.
Исходный код
Вы можете найти исходный код на Github.
Обратная связь
Если эта статья была для вас полезна, вы можете поддержать меня, загрузив из Mac App Store мое приложение для Mac, которое называется FilesApp. FilerApp — это расширение Finder для вашего Mac, которое позволяет вам легко создавать файлы в поддерживаемых форматах в любом месте вашей системы. Оно бесплатно и очень полезно. Надеюсь, вам оно понравится.
Материал подготовлен в преддверии старта онлайн-курса "iOS Developer. Professional".