LoginSignup
12
9

Swiftでバーコードリーダー機能を実装

Last updated at Posted at 2021-10-31

Swiftでバーコードリーダー機能を実装する方法です。

目標

iPhoneのカメラで本の帯の裏に書かれているバーコードを読み込み、13桁の数字のJANコード(isbn13とも言う)を取得する。

実際に実装するコード

以下の6つのステップでご説明します。

  1. モジュールをimport
  2. 変数・定数を定義
  3. セッションを開始する関数を定義・呼び出し
  4. delegateの設定
  5. 画面から離れる直前にセッションを止める
  6. 検出エリアに枠線を表示

それでは順番に見ていきましょう。

1. モジュールをimport

import AVFoundation

まずカメラアプリを導入するために必要なAVFoundationをimportします。

2. 変数・定数を定義

var captureSession : AVCaptureSession?
var videoLayer : AVCaptureVideoPreviewLayer?
var isbn : String?

//検出エリアをカスタマイズする場合のみ使用します
let x: CGFloat = 0.05
let y: CGFloat = 0.4
let width: CGFloat = 0.9
let height: CGFloat = 0.15

次に必要な変数・定数を設定します。

AVCaptureSessionとは
画像や動画といった出力データの管理を行うクラス
AVCaptureVideoPreviewLayerとは
カメラが取得した映像を画面に表示させるクラス
の意味があります。

またカメラ画面に四角い枠を表示させて、検出エリアを設定したい場合は下4行の定数も書いておきましょう。(そっちの方がユーザービリティは高そう)

3. セッションを開始する関数を定義・呼び出し

func startCapture(){

    //画像や動画といった出力データの管理を行うクラス
    let session = AVCaptureSession()
    
    //カメラデバイスの管理を行うクラス
    guard let device : AVCaptureDevice = AVCaptureDevice.default(for: .video) else {
        return
    }
    
    //AVCaptureDeviceをAVCaptureSessionに渡すためのクラス
    guard let input : AVCaptureInput = try? AVCaptureDeviceInput(device: device) else {
        return
    }
    
    //inputをセッションに追加
    session.addInput(input)
    
    //outputをセッションに追加
    let output = AVCaptureMetadataOutput()
    session.addOutput(output)
    
    //取得したメタデータを置くAVCaptureMetadataOutputの設定(delegateの設定)
    output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
    
    //取得したメタデータを置くAVCaptureMetadataOutputの設定(何を検出するか JANコードの場合はean8とean13、他にもqrやcode93などがある)
    output.metadataObjectTypes = [.ean8, .ean13]
    
    //バーコードの検出エリアの設定(設定しない場合、画面全体が検出エリアになる)
    output.rectOfInterest = CGRect(x: y,y: 1-x-width,width: height,height: width)
    
    //セッションを開始
    session.startRunning()
    
    //画面上にカメラの映像を表示するためにvideoLayerを作る
    let videoLayer = AVCaptureVideoPreviewLayer(session: session)
    videoLayer.videoGravity = .resizeAspectFill
    videoLayer.frame = self.view.bounds
    
    //videoLayerを最初に宣言した定数に追加する
    self.videoLayer = videoLayer
    self.view.layer.addSublayer(videoLayer)
    
    //開放用に保持
    self.captureSession = session
    
}

//startCapture関数の呼び出し
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    self.startCapture()
}

startCapture関数はviewDidAppearで呼び出します。

今回はバーコードを読み取るためにmetadataObjectTypesean8ean13を設定していますが、他にもqrpdf417といった種類もあります。

詳しくは公式リファレンスをご覧ください。

4. delegateの設定

startCapture関数でAVCaptureMetadataOutputObjectsDelegateのdelegateをselfに設定しているので、必要なmetadataOutput関数を書きます。

func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection){
    
    //バーコードが検出されたら呼び出される
    for metadataObject in metadataObjects {
        guard self.videoLayer?.transformedMetadataObject(for: metadataObject) is AVMetadataMachineReadableCodeObject else { continue }
        guard let object = metadataObject as? AVMetadataMachineReadableCodeObject else {
            continue
        }
        guard let detectionString = object.stringValue else {
            continue
        }
        //冒頭の文字や文字数を検出して、正しいコードの場合のみ処理が行われるようにしています
        //例えば書籍のJANコード(isbn13)は978から開始される13桁の数字ですので以下のように書いています  
        if detectionString.starts(with: "978") && detectionString.count == 13 {
            self.isbn = detectionString
        }
    }
    //self.isbnが空じゃない時
    if self.isbn != nil {
        print("取得したコードは\(self.isbn)です!")
        //取得し終わったらセッションを止めて空にします
        self.captureSession?.stopRunning()
        self.captureSession = nil
    }
}

5. 画面から離れる直前にセッションを止める

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    self.captureSession?.stopRunning()
    self.captureSession = nil
}

画面から離れるときにはcaptureSessionを止めた上で空にしておきます。

6. 検出エリアに枠線を表示

今の段階では検出エリアをカスタマイズしても、ユーザーからはそのエリアを認識できません。

そのため検出エリアと同じ場所に枠線を表示させて、エリアを分かりやすくする必要があります。

//枠線の表示
let loadArea = UIView()
loadArea.frame = CGRect(x: view.frame.size.width * x, y: view.frame.size.height * y, width: view.frame.size.width * width, height: view.frame.size.height * height)
loadArea.layer.borderColor = UIColor.red.cgColor
loadArea.layer.borderWidth = 4
loadArea.layer.cornerRadius = 6
loadArea.clipsToBounds = true
self.view.addSubview(loadArea)

枠線を表示させるコードはstartCaptureを呼び出した後に書きましょう。

全体コード

最後に今回書いたコードをまとめると以下のようになります。

import UIKit
import AVFoundation

class CaptureViewController : UIViewController {
    
    let detectionArea = UIView()

    let x: CGFloat = 0.05
    let y: CGFloat = 0.4
    let width: CGFloat = 0.9
    let height: CGFloat = 0.15
        
    var captureSession : AVCaptureSession?
    var videoLayer : AVCaptureVideoPreviewLayer?
    var isbn : String?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "バーコード読み取り"
        self.view.backgroundColor = .white
        self.navigationItem.rightBarButtonItem = {
            let btn = UIBarButtonItem(title: "完了", style: .plain, target: self, action: #selector(onPressComplete(_:)))
            return btn
        }()        
    }
    @objc func onPressComplete(_ sender : Any){
        self.captureSession?.stopRunning()
        self.captureSession = nil
        self.dismiss(animated: true, completion: nil)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        self.captureSession?.stopRunning()
        self.captureSession = nil
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.startCapture()
        
        //枠線の表示
        loadArea.frame = CGRect(x: view.frame.size.width * x, y: view.frame.size.height * y, width: view.frame.size.width * width, height: view.frame.size.height * height)
        loadArea.layer.borderColor = UIColor.red.cgColor
        loadArea.layer.borderWidth = 4
        loadArea.layer.cornerRadius = 6
        loadArea.clipsToBounds = true
        self.view.addSubview(loadArea)
    }
    
    func startCapture(){

        //画像や動画といった出力データの管理を行うクラス
        let session = AVCaptureSession()
        
        //カメラデバイスの管理を行うクラス
        guard let device : AVCaptureDevice = AVCaptureDevice.default(for: .video) else {
            return
        }
        
        //AVCaptureDeviceをAVCaptureSessionに渡すためのクラス
        guard let input : AVCaptureInput = try? AVCaptureDeviceInput(device: device) else {
            return
        }
        
        //inputをセッションに追加
        session.addInput(input)
        
        //outputをセッションに追加
        let output = AVCaptureMetadataOutput()
        session.addOutput(output)
        
        //取得したメタデータを置くAVCaptureMetadataOutputの設定(delegateの設定)
        output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
        
        //取得したメタデータを置くAVCaptureMetadataOutputの設定(何を検出するか JANコードの場合はean8とean13、他にもqrやcode93などがある)
        output.metadataObjectTypes = [.ean8, .ean13]
        
        //バーコードの検出エリアの設定(設定しない場合、画面全体が検出エリアになる)
        output.rectOfInterest = CGRect(x: y,y: 1-x-width,width: height,height: width)
        
        //セッションを開始
        session.startRunning()
        
        //画面上にカメラの映像を表示するためにvideoLayerを作る
        let videoLayer = AVCaptureVideoPreviewLayer(session: session)
        videoLayer.videoGravity = .resizeAspectFill
        videoLayer.frame = self.view.bounds
        
        //videoLayerを最初に宣言した定数に追加する
        self.videoLayer = videoLayer
        self.view.layer.addSublayer(videoLayer)
        
        //開放用に保持
        self.captureSession = session
        
    }
}

extension CaptureViewController : AVCaptureMetadataOutputObjectsDelegate {
    
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection){
        
        //バーコードが検出されたら呼び出される
        for metadataObject in metadataObjects {
            guard self.videoLayer?.transformedMetadataObject(for: metadataObject) is AVMetadataMachineReadableCodeObject else { continue }
            guard let object = metadataObject as? AVMetadataMachineReadableCodeObject else {
                continue
            }
            guard let detectionString = object.stringValue else {
                continue
            }
            //冒頭の文字や文字数を検出して、正しいコードの場合のみ処理が行われるようにしています
            //例えば書籍のJANコードは978から開始される13桁の数字ですので以下のように書いています
            if detectionString.starts(with: "978") && detectionString.count == 13 {
                self.isbn = detectionString
            }
        }
        //self.isbnが空じゃない時
        if self.isbn != nil {
            print("取得したコードは\(self.isbn)です!")
            //取得し終わったらセッションを止めて空にします
            self.captureSession?.stopRunning()
            self.captureSession = nil
        }
    }

}

参考

https://motty72.hatenablog.com/entry/2019/01/29/233355
https://ichi.pro/swift-de-ba-ko-do-mataha-qr-ko-do-o-yomitoru-hoho-swift-de-no-puroguramingu-18503546520018
https://qiita.com/_asa08_/items/8562fe79ec6528a61b06
https://www.letitride.jp/entry/2019/12/03/125802

Swiftのお役立ち情報

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9