When you’re building apps, the entry barrier to some features, including text recognition, is high. Even experienced coders take a lot of time and code to get text recognition working in video.
DataScannerViewController from the powerful VisionKit is a self-contained scanner for text and barcodes that removes most of the difficulties from this task.
If you need to get text information into your app, this API might be for you. Inconveniently, DataScannerViewController is a UIViewController and isn’t directly exposed to SwiftUI. That’s OK because UIKit is not going away soon and it’s easy to combine UIKit and SwiftUI.
In this tutorial, you’ll learn how to use and customize DataScannerViewController in a UIKit based app while mixing in SwiftUI components.
To do this tutorial, you need:
- Xcode 14.0.1 or higher.
- An iPhone or iPad running iOS 16 with an A12 Bionic processor or better (Late 2017 forward).
- Basic SwiftUI knowledge.
Getting Started
Slurpy is an app that uses DataScannerViewController to capture text and barcodes and store them for future use. For example, a student visiting a museum could use Slurpy to capture text from exhibit information cards for later use.
Download the project using the Download Materials link at the top or bottom of the tutorial. Open Starter, and then open Slurpy.xcodeproj.
You’ll build to your device for this tutorial. Connect the device to your Mac and select it as the run destination. The name in the bar will be your device’s name.
Select the project file in the Project navigator:
- Select the target Slurpy.
- Switch to the Signing and Capabilities tab.
- Set your own Development Team.
- Change the Bundle ID to your specific team value.
Build and run. You’ll see a premade tabbed interface with two tabs, Ingest and Use, to keep you focused on the cool content. Next, you’ll add DataScannerViewController to your interface.
Using DataScannerViewController
In this section, you’ll create and configure the DataScannerViewController from VisionKit and add it to the interface’s Ingest tab. Soon you’ll be able to see what the camera recognizes in the view.
Creating a Delegate
Delegate protocols, or the delegation pattern, are common all through the Apple SDKs. They help you change a class behavior without needing to create a subclass.
In the Project navigator, in the group ViewControllers, open ScannerViewController.swift. You’ll see an empty class declaration for ScannerViewController
.
Below the line import UIKit
, add the import statement:
import VisionKit
Next, add the following code at the bottom of ScannerViewController.swift:
extension ScannerViewController: DataScannerViewControllerDelegate {
func dataScanner(
_ dataScanner: DataScannerViewController,
didAdd addedItems: [RecognizedItem],
allItems: [RecognizedItem]
) {
}
func dataScanner(
_ dataScanner: DataScannerViewController,
didUpdate updatedItems: [RecognizedItem],
allItems: [RecognizedItem]
) {
}
func dataScanner(
_ dataScanner: DataScannerViewController,
didRemove removedItems: [RecognizedItem],
allItems: [RecognizedItem]
) {
}
func dataScanner(
_ dataScanner: DataScannerViewController,
didTapOn item: RecognizedItem
) {
}
}
In this extension, you conform ScannerViewController
to the protocol DataScannerViewControllerDelegate
. DataScannerViewControllerDelegate
has methods that are called when DataScannerViewController begins recognizing or stops recognizing objects in its field of view.
You’ll come back here later once you have the scanner running. For now, this extension must exist to prevent compiler errors.
Next, you’ll extend DataScannerViewController with a function that instantiates and configures it to your needs.
Extending DataScannerViewController
In this section, you’ll make a DataScannerViewController and set it up to scan text and barcodes.
Add this extension at the bottom of ScannerViewController.swift:
extension DataScannerViewController {
static func makeDatascanner(delegate: DataScannerViewControllerDelegate)
-> DataScannerViewController {
let scanner = DataScannerViewController(
recognizedDataTypes: [
// restrict the types here later
.text()
],
isGuidanceEnabled: true,
isHighlightingEnabled: true
)
scanner.delegate = delegate
return scanner
}
}
In makeDatascanner
, you instantiate DataScannerViewController
. The first argument to init
, recognizedDataTypes
is an array of RecognizedDataType
objects. The array is empty for now — you’ll add items you want to recognize soon.
The arguments isGuidanceEnabled
and isHighlightingEnabled
add extra UI to the view to help you locate objects. Finally, you make ScannerViewController
the delegate of DataScannerViewController
. This property assignment connects the DataScannerViewControllerDelegate
methods you added before.
Adding the Scanner to the View
You’re ready to add the scanner to the view. At the top of ScannerViewController.swift, locate the class declaration for ScannerViewController
and add the following inside the class body:
var datascanner: DataScannerViewController?
You’ll keep a reference to the scanner you create so you can start and stop the scanner. Next, add this method to the class body:
func installDataScanner() {
// 1.
guard datascanner == nil else {
return
}
// add guard here
// 2.
let scanner = DataScannerViewController.makeDatascanner(delegate: self)
datascanner = scanner
addChild(scanner)
view.pinToInside(scanner.view)
// 3.
addChild(scanner)
scanner.didMove(toParent: self)
// 4.
do {
try scanner.startScanning()
} catch {
print("** oh no (unable to start scan) - \(error)")
}
}
In this code you:
- Check for an existing scanner so you don’t add one twice.
- Create a scanner using
makeDatascanner
then pin the view ofDataScannerViewController
inside thesafeAreaLayoutGuide
area ofScannerViewController
.pinToInside
is an Auto Layout helper included with the starter project. - Add your
DataScannerViewController
toScannerViewController
as a child view controller, then tell the scanner it moved to a parent view controller. - Start the
DataScannerViewController
.
Last, you’ll call installDataScanner
when the view appears. Add this code inside the body of ScannerViewController
:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
installDataScanner()
}
You’re ready to fire up the app! Build and run. You’ll see the app immediately crashes with a console message similar to this:
[access] This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.
When an app needs to access the camera, it must explain why it should be permitted. You’ll add the necessary key next.
Adding Camera Usage Description
Now you need to change the Info.plist to get your app working.
- Locate and open Info.plist in the Project navigator.
- Copy this key, NSCameraUsageDescription.
- Select the top level object Information Property List
- Click the + control that appears to add a value to the dictionary.
- In the field that appears, paste the key NSCameraUsageDescription and press Return. You’ll see the key changes to a human-readable value of Privacy — Camera Usage Description.
- Add the description Scan all the things to the Value field.
Build and run. You’ll see a permission alert appear with the text from the camera usage description you added.
Touch OK to grant permission. You now have a working camera.
Point your camera at some text, and you’ll see a bounding rectangle. This behavior is toggled by isHighlightingEnabled
, which you met earlier.
The default state for DataScannerViewController is to recognize everything it can. That’s fun, but it might not be what you want. In the next section, you’ll learn how to limit DataScannerViewController to only recognize what you need.
Restricting Recognized Types
Barcode Symbologies
A barcode symbology is a standard coding method for a piece of data. If you encode your data using QR symbology, anybody with a QR reader can decode it.
For instance, say your museum or library visitor would like to scan some text or the ISBN of a book. An ISBN is a 13-digit number. An ISBN should use EAN-13 symbology in barcode format, so you’ll restrict your scanning to that type.
VNBarcodeSymbology
declares all the types that you can read with VisionKit. Among those types is the EAN-13 standard.
Configuring the Scanner
In ScannerViewController
, locate makeDatascanner
and find the comment // add types here
.
Delete the comment, then add this code to the array in the parameter recognizedDataTypes
:
.barcode(
symbologies: [
.ean13
]),
.text(languages: ["en", "pt"])
You told the DataScannerViewController to look for one type of barcode and English or Portuguese text. Feel free to customize the languages
array with the ISO 639-1 language code for your own country.
Build and run, then scan the barcodes above again. Notice how Slurpy locks onto the barcodes quicker and spends less time jumping around locking onto other items in the field of view.
Customizing the Scanner View
The UI that DataScannerViewController provides is effective, but say you want something else. Pink is hot right now, so you’ll learn to make a custom guide rectangle next.
DataScannerViewController has a property overlayContainerView
. Views placed inside this container won’t interfere with the hit testing in the scanner, which means you can still touch items to add them to your catalog. You’ll make a SwiftUI-based renderer for the recognized items you scan.
Adding a Model
You’re at the point in your app where you need a model layer to keep track of the objects that DataScannerViewController recognizes. The starter project includes a simple model layer to save time and keep the focus on the tutorial topic.
DataScannerViewController uses VisionKit.RecognizedItem
to describe an object that it sees.
In the Project navigator, open Model. Then open TransientItem.swift. TransientItem
is a wrapper around RecognizedItem
. You have this structure so your app isn’t dependent on the data structure of RecognizedItem
.
The next data structure is StoredItem.swift. StoredItem
is Codable
and can be persisted between sessions.
The last file in the Model group is DataStore.swift. DataStore
is an ObservableObject
and a container for both StoredItem
that you want to keep and TransientItem
that DataScannerViewController recognizes during a scanning session.
DataStore
manages access to the two @Published
collections, collectedItems
and transientItems
. You’ll plug it into your SwiftUI code later.
In the next section, you’ll use this model to build an overlay view.
Creating an Overlay View
You’re now ready to create that cool 1980s-inspired interface you’ve always wanted. In the Project navigator, select the Views group.
- Press Command-N to present the File Template picker.
- Select SwiftUI View and press Next.
- Name the file Highlighter.swift and press Create.
In Highlighter.swift, replace everything inside of Highlighter
with:
@EnvironmentObject var datastore: DataStore
var body: some View {
ForEach(datastore.allTransientItems) { item in
RoundedRectangle(cornerRadius: 4)
.stroke(.pink, lineWidth: 6)
.frame(width: item.bounds.width, height: item.bounds.height)
.position(x: item.bounds.minX, y: item.bounds.minY)
.overlay(
Image(systemName: item.icon)
.position(
x: item.bounds.minX,
y: item.bounds.minY - item.bounds.height / 2 - 20
)
.foregroundColor(.pink)
)
}
}
In this View
, you draw a RoundedRectangle
with a pink stroke for each recognized item seen. Above the rectangle, you show an icon that shows whether the item is a barcode or text. You’ll see this in action soon.
Hosting a SwiftUI View
In the Project navigator, open the ViewControllers group and then open PaintingViewController.swift. Add this import above PaintingViewController
:
import SwiftUI
Add this code inside PaintingViewController
:
override func viewDidLoad() {
super.viewDidLoad()
let paintController = UIHostingController(
rootView: Highlighter().environmentObject(DataStore.shared)
)
paintController.view.backgroundColor = .clear
view.pinToInside(paintController.view)
addChild(paintController)
paintController.didMove(toParent: self)
}
Here you wrap Highlighter
in a UIHostingController
and inject the shared instance of DataStore
into the view hierarchy. You’ll use this pattern a few more times in this tutorial.
The general sequence for hosting a SwiftUI View
in a UIViewController
is:
- Create a
UIHostingController
for your SwiftUI view. - Add the
view
of theUIHostingController
to the parentUIViewController
. - Add the
UIHostingController
as a child of the parentUIViewController
. - Call
didMove(toParent:)
to notifyUIHostingController
of that event.
Open ScannerViewController.swift again. Inside the body of ScannerViewController
, add the following property below var datascanner: DataScannerViewController?
.
let overlay = PaintingViewController()
Next in makeDataScanner
, locate the parameter isHighlightingEnabled
and set it to false
so the default UI doesn’t appear under your much better version.
Finally, add this line at the end of installDataScanner
:
scanner.overlayContainerView.pinToInside(overlay.view)
The Highlighter
view is now part of the view hierarchy. You’re almost ready to go.
Using Delegate Methods
Return to ScannerViewController.swift and locate extension ScannerViewController: DataScannerViewControllerDelegate
, which you added earlier. There are four methods in that extension.
The top method is:
func dataScanner(
_ dataScanner: DataScannerViewController,
didAdd addedItems: [RecognizedItem],
allItems: [RecognizedItem]
)
This delegate method is called when DataScannerViewController starts recognizing an item. Add this code to the body of dataScanner(_:didAdd:allItems:)
:
DataStore.shared.addThings(
addedItems.map { TransientItem(item: $0) },
allItems: allItems.map { TransientItem(item: $0) }
)
Here you map each RecognizedItem
to a TransientItem
, then forward the mapped collections to DataStore
.
Next, you’ll complete a similar task for dataScanner(_:didUpdate:allItems:)
, which is called when an item is changed.
Add this code to the body of dataScanner(_:didUpdate:allItems:)
:
DataStore.shared.updateThings(
updatedItems.map { TransientItem(item: $0) },
allItems: allItems.map { TransientItem(item: $0) }
)
Follow up with the third delegate dataScanner(_:didRemove:allItems:)
, which is called when DataScannerViewController stops recognizing an item.
Add this code to the body of dataScanner(_:didRemove:allItems:)
:
DataStore.shared.removeThings(
removedItems.map { TransientItem(item: $0) },
allItems: allItems.map { TransientItem(item: $0) }
)
The final delegate, dataScanner(_:didTapOn:)
, is called when you touch the screen inside a recognized region:
Add this line to the body of dataScanner(_:didTapOn:)
:
DataStore.shared.keepItem(TransientItem(item: item).toStoredItem())
keepItem
uses a StoredItem
because you’re trying to persist the object, so you convert TransientItem
to StoredItem
using a helper.
In that section, you routed the changes from DataScannerViewController to DataStore
, performing all the necessary mapping at the client side.
Build and run to see the new hotness.
You now have a scanner capable of recording text and ISBNs. Next, you’ll build a list to display all the items you collect.
Making a List
You’ll use SwiftUI to build a table and then put that table in the app’s second tab, Use.
Creating a Table
In the Project navigator, select Views, then add a new SwiftUI View file named ListOfThings.swift.
Delete everything inside ListOfThings
, then replace it with:
@EnvironmentObject var datastore: DataStore
var body: some View {
List {
ForEach(datastore.collectedItems, id: \.id) { item in
// 1.
HStack {
Label(
item.string ?? "<No Text>",
systemImage: item.icon
)
Spacer()
ShareLink(item: item.string ?? "") {
Label("", systemImage: "square.and.arrow.up")
}
}
}
// 2.
.onDelete { indexset in
if let index = indexset.first {
let item = datastore.collectedItems[index]
datastore.deleteItem(item)
}
}
}
}
This code generates a List
. The table content is bound to the @Published
array collectedItems
from the DataStore
instance.
- Each cell has a label with an icon at the leading edge and a share icon at the trailing edge. A touch gesture presents a standard iOS share sheet.
- A standard swipe gesture deletes the stored item.
Hosting a Table
Once again, you need to embed ListOfThings
in a UIHostingController
. In the Project navigator, go to ViewControllers and then open ListViewController.swift.
Insert this import above ListViewController
:
import SwiftUI
Add this code inside ListViewController
:
override func viewDidLoad() {
super.viewDidLoad()
let datastore = DataStore.shared
let listController = UIHostingController(
rootView: ListOfThings().environmentObject(datastore)
)
view.pinToInside(listController.view)
addChild(listController)
listController.didMove(toParent: self)
}
That’s the same pattern you used when adding Highlighter
to DataScannerViewController‘s overlay container.
Build and run.
Scan a book barcode and tap on the recognized region. Then, scan a piece of text. If you can’t find any of your own, use the ones above.
Now when you tap on a recognized item, it’s added to the data store. Switch to the Use tab, and you’ll see the items listed.
Touch any item and share the content using a standard share sheet.
Congratulations! You’ve built up the core of your app. You can scan barcodes and text and share the scanned content. The app isn’t quite customer-ready, so next, you’ll perform tasks to make it ready for a wider audience.
Working with Availability and Permissions
In this section, you’ll handle some scenarios where the scanner might not start. There are two main reasons:
- The user’s device is too old and doesn’t support DataScannerViewController.
- The user has declined permission to use the camera or has removed permission to use the camera.
You’ll deal with handling that availability now.
Handling Device Support Checks
You need some UI to display to users when their devices aren’t supported or available. You’ll create a general-purpose banner to use for warning purposes.
In the Project navigator, select Views and add a new SwiftUI View file named FullScreenBanner.swift.
Replace everything inside FullScreenBanner.swift below import SwiftUI
with:
struct FullScreenBanner: View {
var systemImageName: String
var mainText: String
var detailText: String
var backgroundColor: Color
var body: some View {
Rectangle()
.fill(backgroundColor)
.overlay(
VStack(spacing: 30) {
Image(systemName: systemImageName)
.font(.largeTitle)
Text(mainText)
.font(.largeTitle)
.multilineTextAlignment(.center)
Text(detailText)
.font(.body)
.multilineTextAlignment(.center)
.padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20))
}
.foregroundColor(.white)
)
.edgesIgnoringSafeArea(.all)
}
}
struct FullScreenBanner_Previews: PreviewProvider {
static var previews: some View {
FullScreenBanner(
systemImageName: "location.circle",
mainText: "Oranges are great",
detailText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
backgroundColor: .cyan
)
}
}
You declare a View
with a vertical stack of one image and two text blocks. Display the Preview canvas to see what that looks like:
Now, you’ll add a device support check to your application logic.
In the Project navigator, in ViewControllers, open ScannerViewController.swift.
Add this method and property to ScannerViewController
:
var alertHost: UIViewController?
func cleanHost() {
alertHost?.view.removeFromSuperview()
alertHost = nil
}
In cleanHost
, you remove any previously installed view from the view hierarchy of ScannerViewController
.
Add this import below import VisionKit
:
import SwiftUI
Now add these two similar methods to ScannerViewController
:
func installNoScanOverlay() {
cleanHost()
let scanNotSupported = FullScreenBanner(
systemImageName: "exclamationmark.octagon.fill",
mainText: "Scanner not supported on this device",
detailText: "You need a device with a camera and an A12 Bionic processor or better (Late 2017)",
backgroundColor: .red
)
let host = UIHostingController(rootView: scanNotSupported)
view.pinToInside(host.view)
alertHost = host
}
func installNoPermissionOverlay() {
cleanHost()
let noCameraPermission = FullScreenBanner(
systemImageName: "video.slash",
mainText: "Camera permissions not granted",
detailText: "Go to Settings > Slurpy to grant permission to use the camera",
backgroundColor: .orange
)
let host = UIHostingController(rootView: noCameraPermission)
view.pinToInside(host.view)
alertHost = host
}
These two methods configure a FullScreenBanner
and then place that View
into the view hierarchy.
Then add this code to ScannerViewController
:
var scanningIsSupported: Bool {
false
// DataScannerViewController.isSupported
}
var scanningIsAvailable: Bool {
DataScannerViewController.isAvailable
}
DataScannerViewController has isSupported
, a static
property that you can use to query whether the device is up to date. For this run-only, you ignore it and return false
so you can test the logic.
Finally, you must ensure you don’t install the scanner for a nonsupported device to prevent a crash.
Locate installDataScanner
in ScannerViewController
. At the top of installDataScanner
, add this code at the comment // add guards here
:
guard scanningIsSupported else {
installNoScanOverlay()
return
}
guard scanningIsAvailable else {
installNoPermissionOverlay()
return
}
Those two guards prevent you from instantiating DataScannerViewController if the preconditions aren’t met. Camera permissions can be withdrawn at any time by the user, so you must check each time you want to start the camera.
Build and run. You’ll see the view ScanNotSupported
instead of the camera.
Go back to var scanningIsSupported
to remove the mock Boolean value.
- Delete the line
false
. - Uncomment the line
DataScannerViewController.isSupported
.
Build and run.
At this point, you have the option of going to Settings > Slurpy on your device and switching off Allow Slurpy to Access Camera to observe the no permission view in place. If you do, remember to switch permission back on to continue with the tutorial.
Stopping the Scanner
When working with any iOS camera API, it’s good practice to shut the camera down when you finish using it. You start the scanner in viewDidAppear
, so now you’ll stop it in viewWillDisappear
.
Add this code to ScannerViewController
:
func uninstallDatascanner() {
guard let datascanner else {
return
}
datascanner.stopScanning()
datascanner.view.removeFromSuperview()
datascanner.removeFromParent()
self.datascanner = nil
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
uninstallDatascanner()
}
In uninstallDatascanner
, you stop DataScannerViewController and then remove it from the view. You stop using resources that you no longer need.
Providing User Feedback
When you scanned the book barcode and a text fragment, you probably weren’t sure whether the item saved. In this section, you’ll provide some haptic feedback to the user when they save an item.
Providing Haptic Feedback
Haptic feedback is when your device vibrates in response to an action. You’ll use CoreHaptics
to generate these vibrations.
Add this import at the top of ScannerViewController
below import SwiftUI
:
import CoreHaptics
Then, add this property to the top of ScannerViewController
:
let hapticEngine: CHHapticEngine? = {
do {
let engine = try CHHapticEngine()
engine.notifyWhenPlayersFinished { _ in
return .stopEngine
}
return engine
} catch {
print("haptics are not working - because \(error)")
return nil
}
}()
Here you create a CHHapticEngine
and configure it to stop running by returning .stopEngine
once all patterns have played to the user. Stopping the engine after use is recommended by the documentation.
Add this extension to ScannerViewController.swift:
extension ScannerViewController {
func hapticPattern() throws -> CHHapticPattern {
let events = [
CHHapticEvent(
eventType: .hapticTransient,
parameters: [],
relativeTime: 0,
duration: 0.25
),
CHHapticEvent(
eventType: .hapticTransient,
parameters: [],
relativeTime: 0.25,
duration: 0.5
)
]
let pattern = try CHHapticPattern(events: events, parameters: [])
return pattern
}
func playHapticClick() {
guard let hapticEngine else {
return
}
guard UIDevice.current.userInterfaceIdiom == .phone else {
return
}
do {
try hapticEngine.start()
let pattern = try hapticPattern()
let player = try hapticEngine.makePlayer(with: pattern)
try player.start(atTime: 0)
} catch {
print("haptics are not working - because \(error)")
}
}
}
In hapticPattern
, you build a CHHapticPattern
that describes a double tap pattern. CHHapticPattern
has a rich API that’ss worth exploring beyond this tutorial.
playHapticClick
plays your hapticPattern
. Haptics are only available on iPhone, so if you’re using an iPad, you use an early return to do nothing. You’ll soon do something else for iPad.
You start CHHapticEngine
just before you play the pattern. This connects to the value .stopEngine
that you returned in notifyWhenPlayersFinished
previously.
Finally, locate extension ScannerViewController: DataScannerViewControllerDelegate
and add this line at the end of dataScanner(_:didTapOn:)
:
playHapticClick()
Build and run to feel the haptic pattern when you tap a recognized item. In the next section, you’ll add a recognition sound for people using an iPad.
Adding a Feedback Sound
To play a sound, you need a sound file. You could make your own, but for this tutorial, you’ll use a sound that’s included in the starter project.
Go to the top of ScannerViewController.swift and add this import below the other imports:
import AVFoundation
Add this property inside ScannerViewController
:
var feedbackPlayer: AVAudioPlayer?
Finally, add this extension to ScannerViewController.swift:
extension ScannerViewController {
func playFeedbackSound() {
guard let url = Bundle.main.url(
forResource: "WAV_Jinja",
withExtension: "wav"
) else {
return
}
do {
feedbackPlayer = try AVAudioPlayer(contentsOf: url)
feedbackPlayer?.play()
} catch {
print("Error playing sound - \(error)!")
}
}
}
Inside dataScanner(_:didTapOn:)
, below playHapticClick()
add this call:
playFeedbackSound()
Build and run. Ensure that the device isn’t muted and the volume is not zero. When you touch a recognized item, a sound will ring out.
Congratulations! You made a barcode and text scanner focused on students or librarians who want to collect ISBNs and text fragments.
That’s all the material for this tutorial, but you can browse the documentation for DataScannerViewController to see other elements of this API.
In this tutorial, you used a UIKit-based project. If you want to use DataScannerViewController in a SwiftUI project, you’ll need to host it in a UIViewControllerRepresentable
SwiftUI View
. UIViewControllerRepresentable
is the mirror API of UIHostingViewController
.
- Use SwiftUI views in UIKit with
UIHostingViewController
. - Use UIKit view controllers in SwiftUI with
UIViewControllerRepresentable
.
Learning how to implement UIViewControllerRepresentable
is out of scope for this tutorial, but what you’ve learned about DataScannerViewController will apply when you do.
You can download the completed project using the Download Materials link at the top or bottom of the tutorial.
In this tutorial, you learned how to:
- Start and stop DataScannerViewController.
- Work with the data structures that DataScannerViewController provides.
- Integrate SwiftUI views in UIKit components.
- Work with camera hardware availability and user permissions.
- Use haptics and sound to provide user feedback.
DataScannerViewController opens up a world of interaction — at minimal development cost — with the physical and textual worlds.
Have some fun and create a game or use it to hide information in plain sight. A QR code can hold up to 7,000 characters depending on size. If you use encryption only, people with the key can read that data. That’s an information channel even when internet access is blocked, unavailable or insecure.
Please share what you develop in the forum for this tutorial using the link below. I look forward to seeing what you do!