【Swift】AirtableとMoyaを使ったAPI通信


#はじめに
VUI関係の登壇に伴い、スマートスピーカーとiOSアプリ何かで組み合わせたものを簡単に作りたいと思い、Airtableを見つけました。
アプリに取り入れようと思ったところ、Airtableの使い方に関する記事が少なかったため備忘録も兼ねて記事にしました。

・Moya: https://github.com/Moya/Moya　(Airtableとの通信用に利用)

・Airtableとは
[簡単操作でウェブ上にデータベースを作成して視覚的に操作もできる「Airtable」を使ってみた
](https://gigazine.net/news/20171212-airtable/)

#何を作るか

自分が大好きなファミレス、デニーズで何曜日にどこで何を食べたかを記録するアプリ
記録したものを参考にスマートスピーカーがメニューをおすすめ(今回の記事では紹介割愛)


#Airtable準備
今回は初回の準備からTable作成・ドキュメント確認まで説明を記載致します。

[Airtable公式サイト](https://airtable.com)で右上の**Sign up**を押下し必要事項を入力し登録を行う

<img width="500" alt="スクリーンショット 2020-02-16 16.23.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/d856690d-838a-f37f-2cc0-7911f5508ce1.png">

登録後再度サイトを開くとBasesが開かれる。
Workspaceが既にありますが、使い分けるためにWorkspaceを新しく作成したい場合は**Add a workspace**を押下すると新しいWorkspaceが作成できます、(作らなくても可)

下記添付画像のような画面になったら **Add a base** を押下
今回はスマートスピーカー側のAirtableのテストも兼ねており、初期データを楽にいれたかったためcsvを用意していました。
そのため **import a spreadsheet**を押下
(CSVを用意していない場合は **Start from scratch** からぽちぽちTable作れます)

<img width="500" alt="スクリーンショット 2020-02-16 16.27.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/a9a33d92-f86e-6319-5f3e-57c0d062b2bf.png">



**Choose a .CSV file** を押下し、下記のcsvを選択

<img width="521" alt="スクリーンショット 2020-02-16 16.27.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/bd7a503e-91ce-2709-1b4e-056f4819bece.png">


``` Dennys.csv
type,name,week,status
ご飯,とろ〜り卵とチーズのオムライス,日,普通
肉,大盛りカットステーキ,月,空いてる
肉,トロけるお肉のビーフシチュー,火,空いてる
パスタ,熟成卵黄と4種チーズのカルボナーラ,水,普通
ご飯,オマール香るエビドリア,木,普通
デザート,デビルズブラウニーサンデー,金,空いてない
デザート,キャラメルハニーパンケーキ,土,空いてない
```

CSVをいれたあとにTable名を変更したり、項目に間違いがないか確認
(私は店名の項目の入れ忘れに気付いてCSV用意したのに結局ここの画面から項目増やしました…)
<img width="966" alt="スクリーンショット 2020-02-16 16.41.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/b9fbc64c-bdfb-665b-4f88-35a6daf0114d.png">

テーブルの用意ができたらAPIを呼び出すためのドキュメントを確認します。
右上の**HELP** を押下し**API documentation**押下

<img width="1202" alt="スクリーンショット 2020-02-16 16.51.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/41e2c17b-8e70-bcc5-56a0-cefb7b0366b5.png">


APIの使用例がとても分かりやすく書いてあります。
Airtableアカウント画面でAPIkeyを作成していない方は、AUTHENTICATIONの項目の**account**を押下しアカウント画面でAPIkeyを作成してください。

<img width="1315" alt="スクリーンショット 2020-02-16 16.56.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/b1876937-15db-ccb6-dfe8-9e7162939533.png">

今回は用途として、記録をするアプリのため**Create records**を押下しレコードを登録するための情報を確認します。
<img width="1310" alt="スクリーンショット 2020-02-16 17.03.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/752ed3c8-2450-d1f1-32ae-28dee490e57f.png">

requestに必要な情報やサンプルresponseもあるため、ここを見るところまできたらアプリに組み込む準備はばっちりです。


#Xcode準備
Cocoapodsを利用してMoyaをいれる。

```
pod 'Moya', '~> 13.0'
```

#APIの定義

YOUR_TABLEは自身のTableURL(**Create records** 押下時右側記載のURL)
YOUR_API_KEYは自身のAPIkeyをいれます。

``` RecordDennysAPI.swift
enum RecordDennysAPI {
    case PostRecordDennys(type: String, name: String, week: String, status: String, store: String)
}

extension RecordDennysAPI: TargetType {
    
    // ベースURL
    var baseURL: URL {
        return URL(string: "https://api.airtable.com/v0/YOUR_TABLE")!
    }
    
    // パス
    var path: String {
        switch self {
        case .PostRecordDennys:
            return ""
        }
    }
    
    // メソッド
    var method: Moya.Method {
        return .post
    }
    
    // スタブデータ
    var sampleData: Data {
        var path = ""
        switch self {
        case .PostRecordDennys:
            path = Bundle.main.path(forResource: "SuccessRecordDennys", ofType: "json")!
        }
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
    
    // リクエストパラメータ
    var task: Task {
        switch self {
        case .PostRecordDennys(let type,let name,let week,let status,let store):
            let request = PostDennysRecordsRequest(records: [PostDennysRecordsRequest.Fields(fields: PostDennysRecordsRequest.Fields.FieldsElement(type: type, name: name, week: week, status: status, store: store))])
            return .requestJSONEncodable(request)
            
        }
    }
    
    // ヘッダー
    var headers: [String : String]? {
        return ["Content-Type": "application/json",
                "Authorization": "Bearer YOUR_API_KEY"]
    }
}
```

エンコード・デコード用にEncodable・Decodableに準拠したstructを定義。
**Create records**で確認したrequestやresponseを確認しつつ作成。


``` .swift
struct  PostDennysRecordsRequest: Encodable {
    struct  Fields: Encodable {
        struct  FieldsElement: Encodable {
            let type: String
            let name: String
            let week: String
            let status: String
            let store: String
        }
        let fields: FieldsElement
    }
    let records: [Fields]
}

struct  DennysRecordsResponse: Decodable {
    struct  Fields: Decodable {
        struct  FieldsElement: Decodable {
            let type: String
            let name: String
            let week: String
            let status: String
            let store: String
        }
        let id: String
        let fields: FieldsElement
        let createdTime: String
    }
    let records: [Fields]
}
```
#登録項目の入力準備

見た目にはあまりこだわらず、とりあえず店名と料理名以外は常に表示している下部のpickerで選ぶように実装
(本来ならinputviewとかででいい感じにした方が良いと思いますが…)

<img width="279" alt="スクリーンショット 2020-02-16 17.26.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308698/d5427ca6-ee23-3f2b-c1f0-59b40590e33f.png">



``` ViewController.swift
    
    @IBOutlet weak var storeTextField: UITextField!
    @IBOutlet weak var weekTextField: UITextField!
    @IBOutlet weak var dishTypeTextField: UITextField!
    @IBOutlet weak var dishNameTextField: UITextField!
    @IBOutlet weak var stomachStatusTextField: UITextField!
    @IBOutlet weak var choicePickerView: UIPickerView!
    
    // picker用
    let weekList = ["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"]
    let dishTypeList = ["肉","デザート","麺","ドリア","サイドメニュー"]
    let stomachStatusList = ["お腹いっぱい","普通","空いている"]
    var currentSelectedList = [""]
    var currentSelectedTextField = UITextField()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        choicePickerView.delegate = self
        choicePickerView.dataSource = self
        
        // キーボードを非表示にするため
        weekTextField.inputView = UIView()
        dishTypeTextField.inputView = UIView()
        stomachStatusTextField.inputView = UIView()
    }
    
    /// 曜日TextFieldタップ時
    /// - Parameter sender: UITextField
    @IBAction func tapWeekTextField(_ sender: UITextField) {
        currentSelectedList = weekList
        currentSelectedTextField = weekTextField
        choicePickerView.reloadAllComponents()
    }
    
    /// 料理タイプTextFieldタップ時
    /// - Parameter sender: UITextField
    @IBAction func tapDishTextField(_ sender: UITextField) {
        currentSelectedList = dishTypeList
        currentSelectedTextField = dishTypeTextField
        choicePickerView.reloadAllComponents()
    }
    
    /// お腹の具合TextFieldタップ時
    /// - Parameter sender: UITextField
    @IBAction func tapStomachStatusTextField(_ sender: UITextField) {
        currentSelectedList = stomachStatusList
        currentSelectedTextField = stomachStatusTextField
        choicePickerView.reloadAllComponents()
    }

    // キーボード非表示用
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }

// pickerの設定
extension ViewController: UIPickerViewDataSource, UIPickerViewDelegate  {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return currentSelectedList.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return currentSelectedList[row]
    }
    
    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        currentSelectedTextField.text = currentSelectedList[row]
    }
}


```

#API通信の呼び出し
MoyaProviderのインスタンスを作成

``` .swift
    let provider = MoyaProvider<RecordDennysAPI>()
```

登録ボタンタップ時に先ほど作成したPostRecordDennys呼び出す

``` .swift

    /// 登録ボタンタップ時
    /// - Parameter sender: UIButton
    @IBAction func tapRegisterButton(_ sender: UIButton) {
        
        let dishType = dishTypeTextField.text ?? ""
        let dishName = dishNameTextField.text ?? ""
        let week = weekTextField.text ?? ""
        let stomachStatus = stomachStatusTextField.text ?? ""
        let store = storeTextField.text ?? ""
        
        provider.request(.PostRecordDennys(type: dishType,
                                           name: dishName,
                                           week: week,
                                           status: stomachStatus,
                                           store: store)) { (result) in
            
            switch result {
            case let .success(response):
                let decoder = JSONDecoder()
                
                do {
                    let recodeResult = try decoder.decode(DennysRecordsResponse.self, from: response.data)
                    print(recodeResult)
                    //アラート表示
                    self.alert(name: recodeResult.records[0].fields.name)
                } catch let error {
                    print(error)
                }
            case  let .failure(error):
                print(error)
                break
            }   
        }
    }
    
    // 登録完了アラート
    func alert(name: String) {
        let alertController = UIAlertController(title: "デニーズ記録",
                                                message: name + "の登録が完了しました",
                                                preferredStyle: .alert)
        alertController.addAction(UIAlertAction(title: "OK",
                                                style: .default,
                                                handler: nil))
        present(alertController, animated: true)
    }
```

以上で単純なレコードの作成が行えます。
スマートスピーカー開発のためにAirtableに触れましたが、
MoyaでのRESTAPI実装経験も少なかったため、良い勉強になりました。

#参考
- [Voiceflow TIPS #29 Airtableと組み合わせて、もっとデータベースらしく](https://kun432.hatenablog.com/entry/voiceflow_tips_29_integration_with_airtable)
- [Moyaでお手軽API通信](http://gaopin1534.hatenablog.com/entry/2017/05/21/235734)