Skip to main content

Building a Simple Barcode Reader App in Swift

Barcode Reader

The name of the app we’re building today is CDBarcodes – pretty clever. When our device detects a barcode, we will clean up the code and send it to the Discos database to get the album’s name and artist in addition to the year of its release. Discos has a monstrously large database of recorded music, so it’s pretty likely we will get the information that we seek.

Download the starter project for CDBarcodes.

Along with their impressive database, Discos has a useful API that we will query. We will only be touching a little piece of what Discos offers for developers, however, but it’ll be plenty to get a fun little app running.

Discos

First, we’ll need to log in or register for a Discos account. After logging in, scroll to the bottom of the screen. In the footer, in the left-most column, click API.

In the Discos API, again, in the column on the left side, in the Database section, click Search.

This is the endpoint from which we will query. We’ll be getting album information from both the “title” and “year” parameters.

Now would be a good time to record the URL for our query into CDBarcodes. In Constants.swift, add https://api.discogs.com/database/search?q= to DISCOS_AUTH_URL as a constant value.

     let DISCOS_KEY = "your-discos-key"

Now, well be able to access the URL using our handy DISCOS_AUTH_URLthroughout the app.

Back in the Discos API, lets make a new app and get some credentials. In the navigation bar, at the top of the page, tap Create an App. Then, tap the Create an Application button.

For the Application Name, enter “CDBarcodes Your Name”, or some suitable variation. For the description, the following will suffice:
  • “This is an iOS app that reads barcodes from CDs and displays information about the albums.”
  • Then, click the Create Application button.
  • On the resulting screen, we will find the credentials that will allow us to make use of our barcode number.
  • Copy the Consumer Key, and paste it into DISCOS_KEY in Constants.swift.
  • Then, copy the Consumer Secret, and paste it into DISCOS_SECRET in Constants.swift.
As with the URL, we’ll have convenient access to these values throughout CDBarcodes.

CocoaPods

To communicate with the Discos API, we’ll be using the fantastic dependency manager, CocoaPods. For more information on CocoaPods, and for instructions on installing it, have a peek at CocoaPods’ site.

Through CocoaPods, we’ll be leveraging Alamo fire for networking and Swiftly JSON to handle the JSON returned from Discos.

Let’s introduce them to CDBarcodes!

With CocoaPods installed, and an open Terminal, navigate to CDBarcodes and initialize CocoaPods in the Xcode project with the following:

     cd <your-xcode-project-directory>
     pod init

Open the Podfile in Xcode:

     open -a Xcode Podfile

Type or paste the following into the Podfile so it appears as below:

     source 'https://github.com/CocoaPods/Specs.git'
     platform :ios, '8.0'
     use_frameworks!

     pod 'Alamofire', '~> 3.0'

     target ‘CDBarcodes’ do
     pod 'SwiftyJSON', :git => 'https://github.com/SwiftyJSON/SwiftyJSON.git'
     end

Finally, run the following command to download Alamofire and SwiftyJSON:

     pod install

It’s time to get back to Xcode! Be sure to open CDBarcodes.xcworkspace whenever working on the app.

The Barcode Reader

The AV Foundation Framework provides the tools that we will use to build our barcode reader.

Here’s a little rundown of what’s involved in the process.
AVCaptureSession will manage data form the camera – input to output.
The AVCaptureDevice is the physical device and its properties. AVCaptureSession receives input data from the AVCaptureDevice.
AVCaptureDeviceInput captures data from the input device.
AVCaptureMetadataOutput forwards metadata objects to be processed by a delegate object.

In BarcodeReaderViewController.swift, our first step is to import AVFoundation.

    import UIKit
    import AVFoundation

We’ll need to conform to AVCaptureMetadataOutputObjectsDelegate.
In viewDidLoad(), we’ll get our barcode reading engine running.

First, create an AVCaptureSession object and set the AVCaptureDevice. Then, we’ll create an input object and add it to the AVCaptureSession.

class BarcodeReaderViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
   var session: AVCaptureSession!
   var previewLayer: AVCaptureVideoPreviewLayer!

   override func viewDidLoad() {
   super.viewDidLoad()

   // Create a session object.
   session = AVCaptureSession()

   // Set the captureDevice.
   let videoCaptureDevice = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo)

   // Create input object.
   let videoInput: AVCaptureDeviceInput?

   do {
   videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
   } catch {
   return
  }

   // Add input to the session.
   if (session.canAddInput(videoInput)) {

   session.addInput(videoInput)
   } else {
   scanningNotPossible()
}

If, by chance, the device doesn’t have a camera, scanning isn’t possible. As such, we’ll need a failure method. Here, we’ll alert the user to look around for an iOS device with a camera so we can scan and research some CD barcodes.

func scanningNotPossible() {

// Let the user know that scanning isn't possible with the current device.

let alert = UIAlertController(title: "Can't Scan.", message: "Let's try a device equipped with a camera.", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
presentViewController(alert, animated: true, completion: nil)
session = nil
}

Back in viewDidLoad(), after adding the input to the session, we create the AVCaptureMetadataOutput and add it to the session. We will send the captured data to the delegate object via a serial queue.

The next step is to state the type of barcodes for which we should scan. For our purposes, we will be looking for EAN-13 barcodes. Interestingly, not all barcodes that we will scan will be EAN-13 barcodes; some will be UPC-A. This can cause a problem.

Apple converts UPC-A barcodes to EAN-13 by adding a 0 at the front. UPC-A barcodes are only 12 digits; whereas, EAN-13 barcodes are, as one would imagine, 13 digits. The great thing about this automatic conversion is that we can look for metadataObjectTypes AVMetadataObjectTypeEAN13Code, and both EAN-13 and UPC-A barcodes will be read. The tricky part is that this conversion technically changes the barcode and will thoroughly confuse Discos. We’ll be handling this situation shortly, however.

Yet again, if there is an issue with the camera, we’ll need to send the user to scanningNotPossible().

   // Create output object.
   let metadataOutput = AVCaptureMetadataOutput()

   // Add output to the session.
   if (session.canAddOutput(metadataOutput)) {
   session.addOutput(metadataOutput)

   // Send captured data to the delegate object via a serial queue.
   metadataOutput.setMetadataObjectsDelegate(self, queue: dispatch_get_main_queue())

   // Set barcode type for which to scan: EAN-13.
   metadataOutput.metadataObjectTypes = [AVMetadataObjectTypeEAN13Code]
   } else {
   scanningNotPossible()
}

Now that we have this wonderful functionality, we need to see it! We’ll use AVCaptureVideoPreviewLayer to display the video in the full size of the screen.

Finally, we can start the capture session.

   // Add previewLayer and have it show the video data.

   previewLayer = AVCaptureVideoPreviewLayer(session: session);
   previewLayer.frame = view.layer.bounds;
   previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
   view.layer.addSublayer(previewLayer);

   // Begin the capture session.
   session.startRunning()

In captureOutput:didOutputMetadataObjects:fromConnection, we celebrate, as our barcode reader found something!

First, we will get the first object from the metadataObjects array and convert it into machine readable code. Then, we will send that readableCode, as a string, to barcodeDetected().

Before we move on to barcodeDetected(), however, we will provide some user feedback in the form of vibration and stop the capture session. If, by chance we forget to stop the session, get ready for a lot of vibrating. That’s a good reason for a case!

   func captureOutput(captureOutput: AVCaptureOutput!, didOutputMetadataObjects                                       metadataObjects: [AnyObject]!, fromConnection connection: AVCaptureConnection!)
    {

   // Get the first object from the metadataObjects array.
   if let barcodeData = metadataObjects.first {
   // Turn it into machine readable code
   let barcodeReadable = barcodeData as? AVMetadataMachineReadableCodeObject;
   if let readableCode = barcodeReadable {

   // Send the barcode as a string to barcodeDetected()
   barcodeDetected(readableCode.stringValue);
    }

   // Vibrate the device to give the user some feedback.
   AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))

   // Avoid a very buzzy device.
   session.stopRunning()
    }
}

We have some work to perform in barcodeDetected(). Our first task is to follow up our vibration with an alert to state that we’ve found a barcode. Then, we turn our find into something we can really use.

The spaces must be removed from the code. After it is free of white space, we need to determine if the barcode is of the EAN-13 or UPC-A format. If it is EAN-13, we’re already in good shape. If it is a UPC-A barcode, it has been converted to EAN-13, and we need to return it to its original format.

As we’ve discussed, Apple adds a 0 to the front of UPC-A barcodes to convert them to EAN-13, so we should start by determining if there is a 0 at the beginning of the code. If so, we need to remove it. Without this step, Discos won’t recognise the number, and we won’t get our data.

After we get our cleaned-up barcode strings, we ship them off to DataService.searchAPI() and pop BarcodeReaderViewController.swift.

func barcodeDetected(code: String) {

   // Let the user know we've found something.
   let alert = UIAlertController(title: "Found a Barcode!", message: code, preferredStyle:                                         UIAlertControllerStyle.Alert)
   alert.addAction(UIAlertAction(title: "Search", style: UIAlertActionStyle.Destructive, handler: {                 action in

   // Remove the spaces.
   let trimmedCode code.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())

   // EAN or UPC?
   // Check for added "0" at beginning of code.

   let trimmedCodeString = "\(trimmedCode)"
   var trimmedCodeNoZero: String

   if trimmedCodeString.hasPrefix("0") && trimmedCodeString.characters.count > 1 {
   trimmedCodeNoZero = String(trimmedCodeString.characters.dropFirst())

   // Send the doctored UPC to DataService.searchAPI()
   DataService.searchAPI(trimmedCodeNoZero)'

   } else {

   // Send the doctored EAN to DataService.searchAPI()
   DataService.searchAPI(trimmedCodeString)
  }
  self.navigationController?.popViewControllerAnimated(true)
 }))
  self.presentViewController(alert, animated: true, completion: nil)
}

Before moving on from BarcodeReaderViewController.swift, below viewDidLoad(), we will add viewWillAppear() and viewWillDisappear(). viewWillAppear() will start the capture session; whereas, viewWillDisappear()will stop the session.

override func viewWillAppear(animated: Bool) {

   super.viewWillAppear(animated)
   if (session?.running == false) {
   session.startRunning()
   }
}

override func viewWillDisappear(animated: Bool) {

   super.viewWillDisappear(animated)

   if (session?.running == true) {
   session.stopRunning()
   }
}

The Data Service

In DataService.swift, we’ll first import Alamofire and SwiftyJSON.

Next, we will declare some variables to store our raw data, as returned from Discos. As thoughtfully suggested by Bionik6, we’ll use the handy private(set) approach to avoid clogging up the works with getters.

Then, we build our Alamofire GET request. Here, the JSON is parsed, and we get our album’s title and year. We’ll assign the raw title and year strings to ALBUM_FROM_DISCOS and YEAR_FROM_DISCOS, which we will use shortly to initialise our Album.

Now that we have data from Discos, we want to let the world know; well, we really only want to let AlbumDetailsViewController.swift know. That calls for a notification.

import Foundation
import Alamofire
import SwiftyJSON

class DataService 
{
static let dataService = DataService()

private(set) var ALBUM_FROM_DISCOS = ""
private(set) var YEAR_FROM_DISCOS = ""

static func searchAPI(codeNumber: String) {

// The URL we will use to get out album data from Discos
let discosURL = "\(DISCOS_AUTH_URL)\(codeNumber)&?barcode&key=\(DISCOS_KEY)&secret=\(DISCOS_SECRET)"

Alamofire.request(.GET, discosURL)
.responseJSON { response in

var json = JSON(response.result.value!)

let albumArtistTitle = "\(json["results"][0]["title"])"
let albumYear = "\(json["results"][0]["year"])"

self.dataService.ALBUM_FROM_DISCOS = albumArtistTitle
self.dataService.YEAR_FROM_DISCOS = albumYear

// Post a notification to let AlbumDetailsViewController know we have some data.
NSNotificationCenter.defaultCenter().postNotificationName("AlbumNotification", object: nil)
    }
   }
}

The Album Model

In our Album model, Album.swift, we’ll craft the Album to suit our needs. The model will take the raw artistAlbum and albumYear strings and add a touch of user friendly to them. We’ll be able to access the perfected album and yearvariables for display in our AlbumDetailsViewController.swift.

import Foundation

class Album {

private(set) var album: String!
private(set) var year: String!

init(artistAlbum: String, albumYear: String) {

// Add a little extra text to the album information
self.album = "Album: \n\(artistAlbum)"
self.year = "Released in: \(albumYear)"
     }
}

Time to Show Album Data!

In viewDidLoad(), let’s set the labels to point us toward the barcode reader. Then, we need to add the observer for the NSNotification, so our posted notification can be herd. We’ll also remove the observer in deinit.

deinit {

   NSNotificationCenter.defaultCenter().removeObserver(self)
}

override func viewDidLoad() {
super.viewDidLoad()

artistAlbumLabel.text = "Let's scan an album!"
yearLabel.text = ""

NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(setLabels(_:)), name: "AlbumNotification", object: nil)
}

When the notification is observed, setLabels() will be called. Here, we’ll initialise the Album with the raw strings from DataService.swift. The labels will display the perfected strings from the Album.

func setLabels(notification: NSNotification) {

   // Use the data from DataService.swift to initialise the Album.
   let albumInfo = Album(artistAlbum: DataService.dataService.ALBUM_FROM_DISCOGS,    albumYear: DataService.dataService.YEAR_FROM_DISCOGS)
   artistAlbumLabel.text = "\(albumInfo.album)"
   yearLabel.text = "\(albumInfo.year)"
}

Testing CDBarcodes

We have an app! Sure, we could have just flipped the CD over a couple of times to determine the album name, artist, and year of release, but this is way more fun! To best test CDBarcodes, we should find several CDs or records (vinyl). This way, it is more likely that we will encounter examples that have EAN-13 and UPC-A barcodes. We can handle both of them!

When navigating to BarcodeReaderViewController in CDBarcodes, look out for glares and be sure to let the camera focus on the barcode.

Here is the completed version of CDBarcodes.

In Conclusion

The barcode reader is a very useful tool for business people, savvy shoppers, and regular people. For this reason, it’s good for developers to have some experience working with them.

As we’ve seen, though, the fun only starts with the scanner. After the data is received, there can be funny little issues for which we much account in order to use the data, for example, EAN-13 versus UPC-A. We found a way to manipulate the data to suit our needs, and we were good to go.

A great next step would be to explore some other metadataObjectTypes and some new APIs. The possibilities are limitless and the experience is priceless.


Comments

Popular Posts

How I Reduced the Size of My React Native App by 85%

How and Why You Should Do It I borrowed 25$ from my friend to start a Play Store Developer account to put up my first app. I had already created the app, created the assets and published it in the store. Nobody wants to download a todo list app that costs 25mb of bandwidth and another 25 MB of storage space. So today I am going to share with you how I reduced the size of Tet from 25 MB to around 3.5 MB. Size Matters Like any beginner, I wrote my app using Expo, the awesome React Native platform that makes creating native apps a breeze. There is no native setup, you write javascript and Expo builds the binaries for you. I love everything about Expo except the size of the binaries. Each binary weighs around 25 MB regardless of your app. So the first thing I did was to migrate my existing Expo app to React Native. Migrating to React Native react-native init  a new project with the same name Copy the  source  files over from Expo project Install all de...

How to recover data of your Android KeyStore?

These methods can save you by recovering Key Alias and Key Password and KeyStore Password. This dialog becomes trouble to you? You should always keep the keystore file safe as you will not be able to update your previously uploaded APKs on PlayStore. It always need same keystore file for every version releases. But it’s even worse when you have KeyStore file and you forget any credentials shown in above box. But Good thing is you can recover them with certain tricks [Yes, there are always ways]. So let’s get straight to those ways. 1. Check your log files → For  windows  users, Go to windows file explorer C://Users/your PC name/.AndroidStudio1.4 ( your android studio version )\system\log\idea.log.1 ( or any old log number ) Open your log file in Notepad++ or Any text editor, and search for: android.injected.signing and if you are lucky enough then you will start seeing these. Pandroid.injected.signing.store.file = This is  file path where t...

React Native - Text Input

In this chapter, we will show you how to work with  TextInput  elements in React Native. The Home component will import and render inputs. App.js import React from 'react' ; import Inputs from './inputs.js' const App = () => { return ( < Inputs /> ) } export default App Inputs We will define the initial state. After defining the initial state, we will create the  handleEmail  and the  handlePassword  functions. These functions are used for updating state. The  login()  function will just alert the current value of the state. We will also add some other properties to text inputs to disable auto capitalisation, remove the bottom border on Android devices and set a placeholder. inputs.js import React , { Component } from 'react' import { View , Text , TouchableOpacity , TextInput , StyleSheet } from 'react-native' class Inputs extends Component { state = { ...