受信したビーコンを自動的にMySQLに記録する（Swift4/5）

# この記事はなにか

スマホ(iPhone)が特定のiBeaconを感知した場合、MySQLに自動記録するiOSアプリの作り方です。
流れ：`iOS(Swift4/5)⇔PHP⇔MySQL`
出退勤や出入店の自動登録を想定し、狭い場所に設置できるビーコンを使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/b446dcb8-01e1-7f42-e7aa-a3b0a01eb45b.png)

# バージョン情報

- PHP 7.1
- MySQL 5.6.23
- swift4 (2020年1月にswift5でも動作確認済です。)

# 完成イメージ

アプリ画面のイメージです。
ダウンロードして位置情報さえ承認しておけば勝手に記録されていきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/bd160218-31e7-53e9-a378-d972601d51cf.png" width=30%>

## 受信するためのビーコンを準備しよう

iPhoneかiPadにビーコン発信用のアプリをダウンロードして発信します。
`「Beacon入門」`というアプリが個人的にはおすすめです。

iPadやiPhoneがない、ビーコンの発信機を持っていないという場合にMacからビーコンを発信する着想に行き着くと思います。
但し、現時点ではMacOSがmojaveだと難しいようです…
[最近のmacでblenoが使えない2019219](https://qiita.com/n0bisuke/items/67044681dec529c08191#最近のmacでblenoが使えない2019219)

mojave以下では下記記事が参考になるかと思います。
[MacでiBeaconを発信したかったけど苦労した話 | Qiita](https://qiita.com/rm__rf/items/0509684ed04195e99df1#_reference-d7595bfee09960a0684a)


## MySQLを準備しよう

※もし「記録はしなくて受信だけでOK」という方は**Swiftを準備しよう**に進んでください。

今回はシンプルに時間と共にビーコンを近づいたら1、離れたら0と記録することにして、下記のようなSQLを実行します。

```sql
CREATE TABLE beacon (
    id int NOT NULL AUTO_INCREMENT,
    data datetime NOT NULL,
    flag int NOT NULL,
    CONSTRAINT beacon_pk PRIMARY KEY (id)
);
```

SQLを実行するとこのようなものができます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/d5b66fed-73b5-c73b-407a-e8b2af356950.png" width=60%>

## PHPを準備しよう

次にMySQLとiOSアプリをつなぐPHPを準備します。
構造は下記のとおりです。

```
├── api
│   └── recordbeacon.php
└── includes
    ├── Config.php
    ├── DbConnect.php
    └── DbOperation.php
```

MySQLのDBとテーブルを設定します。

```includes/Config.php
<?php
define('DB_USERNAME', 'AAAAAAAA');
define('DB_PASSWORD', 'BBBBBBBB');
define('DB_HOST', 'CCCCCCC');
define('DB_NAME', 'DDDDDDD');
?>
```

次にデータベースと接続するファイルを2つ作成します。

```includes/DbConnect.php
<?php
class DbConnect
{
    private $conn;
    function __construct()
    {
    }
    function connect()
    {
        require_once 'Config.php';
        $this->conn = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME);
        if (mysqli_connect_errno()) {
            echo "Failed to connect to MySQL: " . mysqli_connect_error();
        }
        return $this->conn;
    }
}
?>
```

```includes/DbOperation.php
<?php

class DbOperation
{
    private $conn;

    //Constructor
    function __construct()
    {
        require_once dirname(__FILE__) . '/Config.php';
        require_once dirname(__FILE__) . '/DbConnect.php';
        // opening db connection
        $db = new DbConnect();
        $this->conn = $db->connect();
    }

    //Function to create a new data
    public function beacon($date, $flag)
    {
        $stmt = $this->conn->prepare("INSERT INTO beacon(date, flag) values(?, ?)");
        $stmt->bind_param("si", $date, $flag);
        $result = $stmt->execute();
        $stmt->close();
        if ($result) {
            return true;
        } else {
            return false;
        }
    }
}
?>
```

最後に、値をデータベースに挿入するファイルを作成します。

```api/recordbeacon.php
<?php

//creating response array
$response = array();

if($_SERVER['REQUEST_METHOD']=='POST'){

    //getting values
    $date = $_POST['date'];
    $flag = $_POST['flag'];

    //including the db operation file
    require_once '../includes/DbOperation.php';

    $db = new DbOperation();

    //inserting values
    if($db->beacon($date,$flag)){
        $response['error']=false;
        $response['message']='Data added successfully';
    }else{

        $response['error']=true;
        $response['message']='Could not add data';
    }

}else{
    $response['error']=true;
    $response['message']='You are not authorized';
}
echo json_encode($response);
?>
```

以上でPHPのファイルは完成です。

## サーバーにデプロイしよう

今回作成したPHP、MySqlをAWSなどお好きなレンタルサーバーへデプロイしてください。

## POSTできるか確認しよう

サーバーにアップし、POSTできるか確認のために`POSTMAN`が使えます。
なお、確認できるようであればどのような方法でも大丈夫です。
[POSTMANダウンロード](https://www.getpostman.com/downloads/)

成功すれば何かしらリスポンスが帰ってくると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/83027884-2002-8b3a-32db-1a7d88e013d0.png)

## Swiftを準備しよう

Xcodeでswiftアプリを新規で用意します（名前はご自由に）

`Info.plist`にて下記２つを追加します。

- [Privacy - Location Always and When In Use Usage Description]
- [Privacy - Location When In Use Usage Description]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/00026651-43a6-bfe4-31ca-89be786a09a7.png)

バックグラウンドモードの`Location updates`を実施します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/50774968-3f8a-df39-1374-030b208e1525.png)

`ViewController.swift`に下記コードを書きます。

```ViewController.swift

import UIKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {

    //ビーコン用の変数定義
    var myLocationManager:CLLocationManager!
    var myBeaconRegion:CLBeaconRegion!
    var beaconUuids: NSMutableArray!
    var beaconDetails: NSMutableArray!
    
    //受信したいビーコンのUUIDを下記に記載（最大20個？）
    let UUIDList = [
        "48534442-4C45-4144-80C0-1800FFFFFFFF",
        // この数値はUUIDに応じて変更ください。
    ]
    
    //regionInOut...登録日時の表示
    @IBOutlet weak var dateTimeNow: UILabel!

    //regionInOut...領域内外の表示（outside/inside）
    @IBOutlet weak var regionInOut: UILabel!
    
    //regionNumber...ビーコン領域内外(0 or 1)の表示
    @IBOutlet weak var regionNumber: UILabel!
    
    //distanceBeacon...ビーコンとの距離表示
    @IBOutlet weak var distanceBeacon: UILabel!

    //UUIDの表示
    @IBOutlet weak var uuidNumber: UILabel!
    
    //情報の登録をするためのPHPのURL
    let URL_SAVE_DATA = "https://yourserver/MyWebService/api/recordbeacon.php"
    
    //日時の記録(日本時間)
    func datejapan(){
    let f = DateFormatter()
    f.dateStyle = .short
    f.timeStyle = .medium
    print(f.string(from: Date()))
    }
    //MySQLデータベースに登録するファンクション
    func Register() {
        //created NSURL
        let requestURL = URL(string: URL_SAVE_DATA)
        
        //creating NSMutableURLRequest
        let request = NSMutableURLRequest(url: requestURL!)
        
        //setting the method to post
        request.httpMethod = "POST"
        
        //日時の記録(日本時間)
        let f = DateFormatter()
        f.dateStyle = .short
        f.timeStyle = .medium
        print(f.string(from: Date()))
        
        let Number = regionNumber.text
        let Now = dateTimeNow.text
        
        //creating the post parameter by concatenating the keys and values from text field
        let postParameters = "date="+Now!+"&flag="+Number!;
        
        //adding the parameters to request body
        request.httpBody = postParameters.data(using: String.Encoding.utf8)
        
        //creating a task to send the post request
        let task = URLSession.shared.dataTask(with: request as URLRequest) {
            data, response, error in
            
            if error != nil {
                print("error is \(String(describing: error))")
                return
            }
            
            do { //parsing the response
                let myJSON = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary //converting resonse to NSDictionary
                
                //parsing the json
                if let parseJSON = myJSON {
                    
                    //creating a string
                    var msg : String!
                    
                    //getting the json response
                    msg = parseJSON["message"] as! String?
                    
                    //printing the response
                    print(msg)
                    
                }
            } catch {
                print(error)
            }
            
        }
        //executing the task
        task.resume()
        
    }

    //以下、ビーコン側の処理
    override func viewDidLoad() {
        super.viewDidLoad()
        
        myLocationManager = CLLocationManager()
        // バックグランドモードで使用する場合YESにする必要あり？(追加)
        myLocationManager.allowsBackgroundLocationUpdates = true;
        myLocationManager.delegate = self
        myLocationManager.desiredAccuracy = kCLLocationAccuracyBest
        myLocationManager.distanceFilter = 1
        let status = CLLocationManager.authorizationStatus()
        print("CLAuthorizedStatus: \(status.rawValue)");
        if(status == .notDetermined) {
            myLocationManager.requestAlwaysAuthorization()
        }
        beaconUuids = NSMutableArray()
        beaconDetails = NSMutableArray()
    }
    
    private func startMyMonitoring() {
        for i in 0 ..< UUIDList.count {
            let uuid: NSUUID! = NSUUID(uuidString: "\(UUIDList[i].lowercased())")
            let identifierStr: String = "abcde\(i)"
            myBeaconRegion = CLBeaconRegion(proximityUUID: uuid as UUID, identifier: identifierStr)
            myBeaconRegion.notifyEntryStateOnDisplay = false
            myBeaconRegion.notifyOnEntry = true
            myBeaconRegion.notifyOnExit = true
            myLocationManager.startMonitoring(for: myBeaconRegion)
            uuidNumber.text = UUIDList[0]
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        //日時の記録(日本時間)
        let f = DateFormatter()
        f.dateStyle = .short
        f.timeStyle = .medium
        print(f.string(from: Date()))
        dateTimeNow.text = f.string(from: Date())
        print("didChangeAuthorizationStatus");
        switch (status) {
        case .notDetermined:
            print("not determined")
            break
        case .restricted:
            print("restricted")
            break
        case .denied:
            print("denied")
            break
        case .authorizedAlways:
            print("authorizedAlways")
            startMyMonitoring()
            break
        case .authorizedWhenInUse:
            print("authorizedWhenInUse")
            startMyMonitoring()
            break
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didStartMonitoringFor region: CLRegion) {
        manager.requestState(for: region);
    }
    
    func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) {
        //日時の記録(日本時間)
        let f = DateFormatter()
        f.dateStyle = .short
        f.timeStyle = .medium
        print(f.string(from: Date()))
        switch (state) {
        case .inside:

            //開始：regionNumberに表示(領域内=1)
            regionNumber.text = "1"
            dateTimeNow.text = f.string(from: Date())
            regionInOut.text = "inside"
            
            Register()
            print("iBeacon inside")

            manager.startRangingBeacons(in: region as! CLBeaconRegion)
            break;
        case .outside:
            
            //regionNumberに表示(領域外=0)
            regionNumber.text = "0"
            dateTimeNow.text = f.string(from: Date())
            regionInOut.text = "outside"
            
            Register()
            print("iBeacon outside")
            break;
        case .unknown:

            //regionNumberに表示(領域外=0)
            regionNumber.text = "0"
            dateTimeNow.text = f.string(from: Date())
            regionInOut.text = "outside"
            
            Register()
            print("iBeacon unknown")
            break;
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], in region: CLBeaconRegion) {
        beaconUuids = NSMutableArray()
        beaconDetails = NSMutableArray()
        if(beacons.count > 0){
            for i in 0 ..< beacons.count {
                let beacon = beacons[i]
                let beaconUUID = beacon.proximityUUID;
                let minorID = beacon.minor;
                let majorID = beacon.major;
                let rssi = beacon.rssi;
                var proximity = ""
                switch (beacon.proximity) {
                case CLProximity.unknown :
                    print("Proximity: Unknown");
                    proximity = "Unknown"
                    regionNumber.text = "0"
                    regionInOut.text = "outside"
                    break
                case CLProximity.far:
                    print("Proximity: Far");
                    proximity = "Far"
                    regionNumber.text = "1"
                    regionInOut.text = "inside"
                    break
                case CLProximity.near:
                    print("Proximity: Near");
                    proximity = "Near"
                    regionNumber.text = "1"
                    regionInOut.text = "inside"
                    break
                case CLProximity.immediate:
                    print("Proximity: Immediate");
                    proximity = "Immediate"
                    regionNumber.text = "1"
                    regionInOut.text = "inside"
                    break
                }
                beaconUuids.add(beaconUUID.uuidString)
                var myBeaconDetails = "Major: \(majorID) "
                myBeaconDetails += "Minor: \(minorID) "
                myBeaconDetails += "Proximity:\(proximity) "
                myBeaconDetails += "RSSI:\(rssi)"
                print(myBeaconDetails)
                beaconDetails.add(myBeaconDetails)
                distanceBeacon.text = proximity
            }
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        //日時の記録(日本時間)
        let f = DateFormatter()
        f.dateStyle = .short
        f.timeStyle = .medium
        print(f.string(from: Date()))
        //開始：regionNumberに表示(領域内=1)
        regionNumber.text = "1"
        dateTimeNow.text = f.string(from: Date())
        regionInOut.text = "inside"
        Register()
        print("didEnterRegion: iBeacon found");
        manager.startRangingBeacons(in: region as! CLBeaconRegion)
    }
    
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        //日時の記録(日本時間)
        let f = DateFormatter()
        f.dateStyle = .short
        f.timeStyle = .medium
        print(f.string(from: Date()))
        //regionNumberに表示(領域外=0)
        regionNumber.text = "0"
        dateTimeNow.text = f.string(from: Date())
        regionInOut.text = "outside"
        Register()
        print("didExitRegion: iBeacon lost");
        manager.stopRangingBeacons(in: region as! CLBeaconRegion)
    }
}
```

下記のように画面を作成し、紐づけます。

<img width="1174" alt="スクリーンショット 2019-05-15 23.30.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/13eb8b57-85f9-11d9-cca2-8893fad4172b.png">

## さあ動かそう！

- ご自身のスマホに今回作成したアプリをビルドします。
- アプリを起動し、位置情報の使用を承認します。
- ビーコンを発信します。
- 領域、距離、UUIDが変更されれば成功です:sunny:

そしてMySQLはというと…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224314/69b2dff6-21ea-b05a-8037-052a50e92066.png)
お！記録されていますね！成功です！

## 所感

Swiftまわりがあまりわからない状態で実施したので、下調べが大変でした…笑
（不足、タイポあれば再記載します）
`Apple Developer Program`に入られている方は通知機能入れても面白いと思います。

## 参照

- [Swift PHP MySQL Tutorial – Connecting iOS App to MySQL Database | Simplified iOS by Belal Khan](https://www.simplifiedios.net/swift-php-mysql-tutorial/)
- [(初心者向け)Swift3.0で初アプリ - ビーコンを検出してみる｜Qiita](https://qiita.com/egplnt/items/b9deefa85992bdad356f)