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

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

## 目標

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

## 実際に実装するコード

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

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

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

### 1. モジュールをimport

```swift
import AVFoundation
```

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

### 2. 変数・定数を定義

```swift
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`とは
カメラが取得した映像を画面に表示させるクラス
の意味があります。

https://developer.apple.com/documentation/avfoundation/avcapturesession

https://developer.apple.com/documentation/avfoundation/avcapturevideopreviewlayer

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

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

```swift
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で呼び出します。

今回はバーコードを読み取るために`metadataObjectTypes`で`ean8`と`ean13`を設定していますが、他にも`qr`や`pdf417`といった種類もあります。

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

https://developer.apple.com/documentation/avfoundation/avmetadatamachinereadablecodeobject/machine-readable_object_types

### 4. delegateの設定

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

```swift
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. 画面から離れる直前にセッションを止める

```swift
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    self.captureSession?.stopRunning()
    self.captureSession = nil
}
```
画面から離れるときにはcaptureSessionを止めた上で空にしておきます。

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

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

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

```swift
//枠線の表示
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を呼び出した後に書きましょう。

## 全体コード

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

```swift
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のお役立ち情報

https://wagtechblog.com/programing/swift-iphone-application-school

https://wagtechblog.com/programing/swift-textbook.html