【初心者向け】RailsとSwiftで動画アップローダーを作る🚀

# まえがき

知らぬ間にまたアドベントカレンダーの季節ですね🙄
今年もノリと勢いが前のめりすぎて立候補しましたがギリギリです。
(いつになったら一日は３０時間になるのか。。)
(ちなみに去年こんなの書きました[10分クッキング！誰でもインフルエンサーになれるinstabotの作り方🍳](https://qiita.com/mopiemon/items/116dcd4edb83f9db31c9))

今年のテーマは、**動画アップロード機能** です。
画像は馴染みがあるけど動画はやったことないなんて人、多くないでしょうか？
そんな私を含めた方達に向けて、ゆるく解説していこうと思います◎

👇こんなの作ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/d58601c2-ef16-9bd7-1932-6a0833eadbb9.png)
機能は、**動画選択・プレビュー・アップロード・最新の動画をフェッチして再生**です。

たぶんこんなに細かく解説してる記事はレアなので、是非お付き合いください🚀
(あといいね欲しいですね。。！もう押してもらって大丈夫です。。🎅🏻！)

# お勧めしたい人

- Ruby(Rails)やったことあるよ
- HTTPリクエストなんとなくわかるよ
- RailsのAPIモードちょっと触ってみたいよ
- モバイルアプリやってみたいよ
- Swiftやってみたいよ
- Swift初心者だよ
- 動画アップロードやってみたいよ
- というか開いてくれた人 **みんな**ですね！

(せっかく一年に一回のアドベントカレンダーってお祭りなので、みんなに読んで欲しいです！おねがいします！)

ようこそ〜〜〜🍺

# 学べること
(めっちゃ長いので、お急ぎの方はここから遷移してください🎅🏻)
### Swift
- [モバイルアプリを作る](#いざ実装)
- 動画を扱う([選択](#動画選択の機能を作成する)・[再生](#動画再生の機能を作成する)・[送信](#動画アップロードの機能を作成する))
- [http通信を許可する(localhost:3000にアクセスできるようにする)](#http通信の許可)
- [APIクライアントを作る](#2SwiftAPIクライアントの実装)

### Ruby on Rails

- [APIサーバーを作る](#3railsapiサーバーの実装)
- [ActiveStorageを使って動画を保存する](#activestorageの設定)

### その他
- [アップロードする時のファイルの扱い方(base64とmultipart/form-dataの違い)](#動画データの扱い方について)
- [ソースコード](#ソースコード)

# 使用技術

サーバーサイド(API)に**Ruby**
フロントエンド(モバイル)に**Swift**の構成です。

Ruby 2.6.2
Ruby on Rails 5.2.3
Swift 5.0.1
Xcode 10.2.1
(ちょっと古いので上げます🥺)

まだまだやれるぜって人へ
実際にアプリをストアにあげるのに、Railsプロジェクトのデプロイは必須なのでHerokuデプロイとか挑戦してみてください🍺
[【初心者向け】railsアプリをherokuを使って確実にデプロイする方法【決定版】](https://qiita.com/kazukimatsumoto/items/a0daa7281a3948701c39)

# いざ実装🚀🚀

## 全体のザックリした流れ
1. Swiftで動画アップローダーを作る
2. SwiftでAPIクライアントを作る
3. RailsでAPIサーバーを作る
4. Swiftで動画プレイヤーを作る

~ 完成 ~

# 1.[Swift]動画アップローダーの実装🚀

## ザックリした流れ
1. プロジェクトの作成
2. シュミレーターにサンプル動画を追加
3. 今回使用するファイルの説明
3. 動画選択の機能を作成する
4. 動画再生の機能を作成する
5. 動画アップロードの機能を作成する

## プロジェクトの作成
1. Xcodeを起動する
2. **Create a new Xcode project**を選択する
3. **Single View App**を選択して**Next**を選択する
4. Product Name(好きなアプリ名)を入力して**Next**を選択する
5. 保存先を指定して**Create**を選択する

これでSwiftプロジェクトの作成ができました！

## シュミレーターにサンプル動画を追加
**シュミレーター**とは、書いたプログラムの動作を、mac内で確認するためのものです。
(Railsで動作確認する時、ブラウザにlocalhost:3000と入力して見る画面のようなものです。)

シュミレーターには元々サンプル画像しか入っていないので、ここで動画を追加します。
まだ何もコードは書いていませんが起動(ビルド)してみましょう！

1. 起動したいデバイス(自分はiPhoneXR)を指定して左上の再生ボタンを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/f2360439-3eed-9d02-1cc7-0f193606856c.png)
2. シュミレーターを起動したら真っ白な画面が出ますがメニューに戻り、写真アプリを開きます
3. macのfinderから好きな動画を選択します(なければスマホなどからmacに送ってください)
4. シュミレーターにドラッグアンドドロップで追加します

こんな感じで入ればOK！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/2663a2f9-9c80-954d-51c1-8928f9116958.png)

## 今回使用するファイルの説明
ここから実際に画面を作っていきますが、
先に今回使用するファイルについてザックリ説明しておきます。

- **Main.storyboard**(自動生成・自作も可)
**storyboard**は**UI部品**(ボタンや画像表示するのに必要なパーツなど)を置いて、
直感的にレイアウトを作成するファイルです。
細かい機能やレイアウトを実装はこのファイルでは実装しきれません。あくまでレイアウトをザックリ構築するファイルです。

- **ViewController.swift**(自動生成・自作も可)
storyboardでカバーできない機能的な実装や細かいレイアウトを記述するファイルです。
例えば、ボタンが押された時の挙動やAPIから受け取ったデータをUIに受け渡す処理なんかを書きます。

今回は**VideoUploaderViewController.swift**を作成します。

- **APIClient.swift**(自作)
バックエンド(APIサーバ)へのリクエストやレスポンスを受け取るのに使用します。

- **Info.plist**(自動生成・自作も可)
設定ファイルです。今回はここでフォトライブラリへのアクセス許可の設定をします。

## 動画選択の機能を作成する

ここでやること
1. VideoUploaderViewController.swiftを作成する
2. Main.storyboardに動画選択用のボタンを追加する
3. ユーザーにカメラロールの使用許可をとる実装を追加する
4. ボタンをViewControllerに接続する
5. ボタンが押されたときにフォトライブラリを開いて動画を選択する
6. 選択された動画のサムネイルをViewControllerに表示する

### 1.VideoUploaderViewController.swiftを作成する
基本的に、１ページを作るのに**UI部品を配置するStoryboard(ViewController)**と**機能を実装するViewController**がセットで必要です。
はじめに**動画の選択からアップロードまでを行うページ**を作っていきます。
このページは
storyboardに自動生成されるMain.storyboard(ViewController)と、
ViewControllerは自動生成されるViewController.swiftをリネームして作成していきます。

storyboardは自動生成なので、まずはViewController.swiftをリネームていきます。
Xcodeの左側にあるナビゲーターからプロジェクターナビゲーター
(左上のファイルアイコンを押すと出てきます。ファイルの一覧です)を選択し、
ViewControllerをクリックして名前を**VideoUploaderViewController.swift**に変更してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/0e17a62f-7e7d-b37a-add1-0ad2e03c2f96.png)

ファイル内のclass名も変更します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/3471bcae-6edc-89ad-d5e6-802d6f5f6013.png)

次に、セットで使用するstoryboardに紐づいているViewController名を変更します。
左側のナビゲーターからMain.storyboardを選択します。ここにはViewControllerの白いパーツがあるだけかと思います。
この黄色いアイコンを選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/1e09fb7e-f698-a6f9-bd4a-6ea9b9f20a5a.png)

右側のインスペクターで、**Identity Inspector**の**Custom Class**を変更します。
ここを先ほどの**VideoUploaderViewController.swift**に変更すると接続が完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/7745ab0f-a8a5-15b5-2322-bde1d67eb3a5.png)

###2.Main.storyboardに動画選択用のボタンを追加する
Main.storyboardを開いているのでついでに動画選択のボタンを設置していきます。

Libraryを開いて**UIButton**と検索してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/fbe3512b-13f1-a28d-e652-2bccfd2a77f7.png)

これをドラッグアンドドロップでViewController内におきます。
青い淵の状態だとパーツが固定されていないのでUI部品に制約(縦横のサイズやトップからの距離などのルール)を追加していきます。
基本的に必要な制約は、縦横・座標(画面に対しての位置)です。

**width/height**
widthを設定します。
Ctrを押しながらボタンを選択、ボタン内で選択をやめるとこのようなメニューが出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/9ac4474b-a578-b4aa-7832-86b193d98c6d.png)
ここでwidthを選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/7cc4465d-2614-a5ca-a1fd-46f24b473925.png)
赤くなっているのは無視してください。(ちゃんと制約が指定できたら青くなります！)
制約部分を選択すると右側に**Attributes Inspector**が開きます。
ここで**Constant**を**80**にしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/46f787c8-eaa3-a82b-b943-5f597101a87e.png)

同じようにして、heightのConstantを80で指定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/91abc17e-71af-a391-7d96-5fed09a1c744.png)

**座標**
次にボタンの座標を決めていきます。
画面のトップやボトムから距離を決めたり、画面の真ん中で指定したり方法は色々あります。
今回はトップからの距離と左からの距離を指定します。
先ほどと同じように、Ctrを押しながらボタンを選択、
今度はボタンの外までカーソルを引っ張り選択を解除します。
以下のメニューが出てくるので**Top Space to Safe Area**を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/c9082326-198c-af3f-0e76-be374313a9c7.png)
これもConstantを548に変更してください。

同じように画面の左からの制約を付けていきます。
メニューを開いたら、今度は**Leading Space to Safe Area**を選択してください。
ここはConstantを71に指定してください。

最後にbuttonという文字をクリックして、Selectに変更してください。
これでボタンの追加は終了です。(好きに色とか付けてもらったり、位置も自由で大丈夫です。)
(指定に中途半端な値を指定しているのは、デモで作ったアプリを
[AutoLayout](https://type.jp/et/feature/3112)で実装しているのですが、そのレイアウトに近づけるためです。(起動に使う端末はXRです。)
**AutoLayout**とは、UI部品同士を相対的に配置して、どのサイズの端末でも同じような配置でレイアウトを実現できる機能です。
後でソースコードを載せるので参考にしてみてください🙇🏻‍♀️)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/a5046a41-d71f-7397-32dd-135ddafdec5d.png)

### 3.ユーザーにカメラロールの使用許可をとる実装を追加する
カメラアプリをインストールした時にこのようなモーダルに遭遇することがあると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/82f4e9c9-270d-54f4-39a6-ab2f9ddf2534.png)
ユーザーから、フォトライブラリにアクセスする許可を取らないと、
動画を選択することができないのでこれを実装していきます。

まず、**Info.plist**でフォトライブラリを使用する利用目的を設定します。
アプリ名のディレクトリ以下の**Info.plist**を開いてください。(テスト用のディレクトリにもあるので注意してください)
**Information Property List**の横にある+ボタンを選択して、**Privacy - Photo Library Usage Description**を入力してください。
行が追加されたら、Valueの部分には利用目的を記述します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/9230bde4-f67f-41bd-f141-ff2c6ad0244e.png)

ここまでで**Info.plistの設定**は終わりです。

次に、**アラートを出す**実装をします。
iOS11以降.plistに設定してもアクセス権限の確認が自動的に行われなくなったので
意図的にタイミングを指定してアラート表示する必要があります。
最初の画面が表示されたタイミングでアラートを表示するようにしていきましょう。

最初に開かれるページになる、**VideoUploaderViewController.swift**を開いてください。

今回必要なフォトライブラリへアクセスするのでPhotosというフレームワークを使用します。
Appleが用意してくれているものなのでimport Photosを記述することで使用できるようになります。
以下のように追加してください。

```swift:VideoUploaderViewController.swift
import UIKit
import Photos
```

自動生成されるviewDidLoadの下に、confirmPhotoLibraryAuthentication()という関数を作成します。
ここで、フォトライブラリの使用許可の確認を行います。

```swift:VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthentication() {

    }
```
**private**は、このclass内でのみ呼び出しを許可したい時に使用します。
(railsでもストロングパラメーターを記述する時に使ったことがあるかと思います。同じやつです。)

この関数内で、アクセス許可をされているかの**現状の確認**と**許可されていなかった時の挙動**を記述していきます。

```swift:VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthenticationStatus() {
        //権限の現状確認(許可されているかどうか)
        if PHPhotoLibrary.authorizationStatus() != .authorized {
            //許可(authorized)されていない・ここで初回のアラートが出る
            PHPhotoLibrary.requestAuthorization { status in
                switch status {
                //もし状態(status)が、初回(notDetermined)もしくは拒否されている(denied)の場合
                case .notDetermined, .denied:
　　　　　　　　　　　　//許可しなおして欲しいので、設定アプリへの導線をおく
                    self.appearChangeStatusAlert()
                default:
                    break
                }
            }
        }
    }
```
`PHPhotoLibrary.requestAuthorization`の時に先ほどのアラートが出現します。
その結果をstatusとしてクロージャー({})内に展開します。
このアラートは基本的に一度きりなので
アプリを閉じられたりして初回(notDetermined)のままだったり
アクセスを許可していない(denied)ユーザーに対して再度設定を促すアラートを出そうと思います。

**appearChangeStatusAlert()**という関数を作成します。
confirmPhotoLibraryAuthenticationStatus関数の下に追加してください。

```swift:VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthenticationStatus() {
        //略
    }

　　 //ここから
    private func appearChangeStatusAlert() {
        //フォトライブラリへのアクセスを許可していないユーザーに対して設定のし直しを促す。
        //タイトルとメッセージを設定しアラートモーダルを作成する
        let alert = UIAlertController(title: "Not authorized", message: "we need to access photo library to upload video", preferredStyle: .alert)
        //アラートには設定アプリを起動するアクションとキャンセルアクションを設置
        let settingAction = UIAlertAction(title: "setting", style: .default, handler: { (_) in
            guard let settingUrl = URL(string: UIApplication.openSettingsURLString) else { return }
            UIApplication.shared.open(settingUrl, options: [:], completionHandler: nil)
        })
        let closeAction = UIAlertAction(title: "cancel", style: .cancel, handler: nil)
        //アラートに上記の２つのアクションを追加
        alert.addAction(settingAction)
        alert.addAction(closeAction)
        //アラートを表示させる
        self.present(alert, animated: true, completion: nil)
    }
```
ここまで書けたら、画面が読み込まれた際に呼び出される**viewDidLoad()**から
**confirmPhotoLibraryAuthenticationStatus()**を呼び出してみましょう。

```swift:VideoUploaderViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // MARK: allow access to camera roll
        self.confirmPhotoLibraryAuthenticationStatus()
    }

```

実際に起動して、動作を確認してみましょう。
Info.plistで指定した利用目的はここに出てきます！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/335e75bd-b60d-f77c-f0fe-fded6ce0c43d.png)

Don't Allowを押すと先ほどタイトルなどを設定したアラートが出現します。
settingを押すと設定アプリも開けるようになってると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/dcb4bbd8-3f13-ad05-1ee2-e032e5adb6b3.png)

デバッグの注意点:
今回の実装では最初のアラートは初回起動時一回しか出てこないので、
再度確認したいときは、シュミレーターからアプリを削除してビルドし直してください。


### 4.ボタンをViewControllerに接続する
ユーザーからフォトライブラリへのアクセス許可を貰えるようになったので、
ここからはフォトライブラリから動画を選択する機能を作っていきます。

まず**VideoUploaderViewController.swiftと先ほどstoryboardに配置したボタンを接続**していきます。
Main.storyboardを開いてください。
optionを押しながら、VideoUploaderViewController.swiftを開きます。
左側に**Main.storyboard**、右側に**VideoUploaderViewController.swift**が開けていればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/c31a9c90-1e32-d125-f2ce-524ccda48d6b.png)

storyboardでCtrを押しながらボタンを選択して、ViewcontrollerのviewDidLoadの上まで
カーソルを引っ張ってください。
選択をやめると、このようなメニューが出てくるかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/f2bc3345-71e3-273d-2063-c4e20473f49a.png)
以下のように**Connection**を`Action`に変更して、
**Name**に`didTapSelectButton`と入力してConnectを押してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/2917b4ec-bf43-688c-f5bf-d9dc84cea7bf.png)
**Connection**に`Action`を指定することで
ボタンをタップされた後に呼び出される関数を自動生成することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/8aa474e3-eea6-24f1-45f8-de84d4f341bb.png)

ここまでで、VideoUploaderViewController.swiftと動画選択のボタンの接続は終わりです。

### 5.ボタンが押されたときにフォトライブラリを開いて動画を選択する

ここでやることは、**UIImagePickerController**を使って
フォトライブラリから動画を選択できるようにします。

[**UIImagePickerController**](https://developer.apple.com/documentation/uikit/uiimagepickercontroller)とは、
**メディア周り(フォトライブラリにアクセスしたりやカメラを起動したり)の機能を簡単に扱えるようにするクラス**です。Photosフレームワークをインポートしてあるだけで使用できます。
(今回はフォトライブラリのアクセス許可をとる時に既にインポートしてあるのですぐに使えます。）

早速実装していきます。
VideoUploaderViewControllerクラスに、UIImagePickerControllerインスタンスを生成します。

```swift:VideoUploaderViewController.swift

class VideoUploaderViewController: UIViewController{

    let imagePickerController = UIImagePickerController()
    //略

}
```

次に、VideoUploaderViewController.swiftに**selectVideo**関数を作成します。
ここにフォトライブラリを開いて動画を選択する機能を実装していきます。

```swift:VideoUploaderViewController.swift
    private func appearChangeStatusAlert() {
        //略
    }
    //ここから
    private func selectVideo() {
        //選択できるメディアは動画のみを指定
        self.imagePickerController.mediaTypes = ["public.movie"]
        //選択元はフォトライブラリ
        self.imagePickerController.sourceType = .photoLibrary
        //実際にimagePickerControllerを呼び出してフォトライブラリを開く
        self.present(self.imagePickerController, animated: true, completion: nil)
    }
```

最後に、自動生成した**didTapSelectButton**の関数から**selectVideo()**を呼び出すことで
ボタンがタップされた時にフォトライブラリを開き動画を選択することができるようになります。

```swift:VideoUploaderViewController.swift
　　 //選択ボタンがタップされた時に呼び出される
    @IBAction func didTapSelectButton(_ sender: Any) {
        selectVideo()
    }
```
ビルドして確認してみましょう。動画を選択できるようになったと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/3f129d0b-ce5f-2df0-4bff-ca2a6d47811a.png)

### 6.選択された動画のサムネイルをViewControllerに表示する
最後に選択した動画のサムネイルをVideoUploaderViewControllerに表示します。
少しやることが多いので、流れをまとめます。以下の通りです。
- Main.storyboardにサムネイルを表示するためのUIImageViewを置く
- UIImageViewをViewControllerに接続する
- ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる
- imagePickerControllerが閉じられる時に呼び出される関数を実装する
- 取得した動画のパスを元にサムネイルを生成する


#### Main.storyboardにサムネイルを表示するためのUIImageViewを置く
Main.storyboardを開いてください。
今回はサムネイル(画像)を表示することが目的です。画像を表示するにはUIImageViewを使用します。
UIButtonの時と同じように、LibraryからUIImageViewを検索して追加します。
**高さ**はConstantを250[ポイント](https://ja.stackoverflow.com/questions/24938/ios-view%E3%81%AE%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)に指定してください。
**横幅**は画面いっぱいに指定してください。
画面いっぱいにするには、Ctrを押しながらUIImageViewを選択してカーソルをUIImageViewの外側まで持っていきます。
ここで選択を解除すると以下のメニューが出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/197ce279-c8ec-499e-c6da-e3ec66b35077.png)
**Equal Widths**を選択すると画面に対して横幅がいっぱいになります。

次に**横軸の座標**を指定します。今回は画面に対して中央にします。
同様にメニューを出して**Center Horizontally in Safe Area**を指定してください。
このように真ん中にUIImageViewが移動すると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/e74d3f54-b132-062f-5aac-d419760ac897.png)
最後に**縦軸の座標**を指定します。今回は動画選択のボタンのトップよりUIImageViewのボトムが54ポイント上になるように指定します。
Ctrを押しながら、UIImageViewを選択してください。カーソルは選択ボタンまで持っていってメニューを表示させます。
ここで**Vertical Space**を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/f15556bc-6147-eff4-b99e-3189cbaf2eb9.png)
Constantは54ポイントを指定してください。

これでUIImageViewが置けました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/fcc3c3c9-6038-13b3-8eca-d728fe84c126.png)

#### UIImageViewをViewControllerに接続する
選択ボタンの時と同じようにMain.storyboardを開いて、隣にVideoUploaderViewcontroller.swiftを開いてください。
(optionを押しながらVideoUploaderViewcontroller.swiftを選択してください)
Ctrを押しながら、UIImageViewをVideoUploaderViewcontroller.swiftに繋いでください。
今度は**Connection**は`Outlet`のままで、Connectしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/6c6000e9-0991-88ea-35a0-edce84c969d6.png)
以下のコードが生成されたら接続は完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/8d971a9e-c642-c7be-8a3d-64ae5841b297.png)

#### ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる

ここが一番わかりづらいと思います🥶
そもそも**Delegate(デリゲート)**とはSwiftでよく使われる考え方で、ここら辺の記事がわかりやすいです。
[【swift】イラストで分かる！具体的なDelegateの使い方。
](https://qiita.com/narukun/items/326bd50a78cf34371169)
簡単にいうと、あるクラスが別のクラスに処理をまかせる(委譲する)ことです。
なんで批准という表現が使われるかという話は、例え話がわかりやすいです。
今回扱うデリゲートやプロトコルは、所謂条約のような決まり事です。
必ず守らなければならないルールがあったり、出来ることが増えたりします。
またそこには加盟する国々(ViewControllerなど)がいます。
この関係を国々が条約に批准するというように、
ViewControllerもデリゲートやプロトコルに批准すると考えると理解しやすいです！(個人談)

今回は、デリゲートに批准することで、VideoUploaderViewControllerに
UIImagePickerControllerの関数を使って、選択した動画やサムネイルを受け取ってもらいます。

実装方法は、まずクラスを定義している部分にUIViewControllerと同じように必要なDelegateを記述します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/3871f627-439a-3419-47db-56a548b605ea.png)

次に、imagePickerControllerの代わりにVideoUploaderViewControllerが役割を肩代わりしますよ！って宣言をします。

```swift:VideoUploaderViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        self.imagePickerController.delegate = self
    //略

```
これで批准完了です！

#### imagePickerControllerが閉じられる時に呼び出される関数を実装する
まず、選択された動画のurlを保持するための変数を定義します。

```swift:VideoUploaderViewController.swift
class VideoUploaderViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
    //ここを追加
    private var videoUrl: NSURL?
```

先ほどのデリゲートに批准したので、動画を選択した後imagePickerが閉じられる時に呼び出される関数を利用できるようになります。

```swift:VideoUploaderViewController.swift
　　 //imagePickerが閉じられる時に呼ばれる
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
　　　　　//キーを指定して選択された動画のパスを取得する
        let key = UIImagePickerController.InfoKey(rawValue: "UIImagePickerControllerMediaURL")
        videoUrl = info[key] as? NSURL
　　　　　//動画の絶対パスを元にサムネイルを生成(generateThumbnailFromVideoの実装は後述します。)
        //先ほど接続したthumbnailImageViewのimageにサムネイルをセット
        thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)
　　　　　//サムネイルの縦横比を変えずに長い辺を画面サイズに合わせる設定
        thumbnailImageView.contentMode = .scaleAspectFit
　　　　　//imagePickerControllerを閉じる
        imagePickerController.dismiss(animated: true, completion: nil)
    }
```

#### 取得した動画のパスを元にサムネイルを生成する
先ほど後述すると書いたgenerateThumbnailFromVideoという関数を実装します。

```swift:VideoUploaderViewController.swift
    private func generateThumbnailFromVideo(_ url: URL) -> UIImage? {
        //以下の３行で縦動画から画像を取り出しても横向きの画像にならないようにしてる
        let asset = AVAsset(url: url)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
　　　　　//縦動画からサムネイルを切り出した時に正しい向きで表示させる設定
        imageGenerator.appliesPreferredTrackTransform = true
        //切り取るタイミングの指定
        var time = asset.duration
        time.value = min(time.value, 2)
　　　　　//サムネイルの生成
        do {
            let imageRef = try imageGenerator.copyCGImage(at: time, actualTime: nil)
            return UIImage(cgImage: imageRef)
        } catch {
            return nil
        }
    }
```
この関数は`-> UIImage? `でサムネイルを返り値として指定しています。
生成に成功すると`return UIImage(cgImage: imageRef)`で画像を返してくれます。
これが先ほど実装した関数のこの部分に返るのでサムネイルが表示されるようになります。
`thumbnailImageView.image = generateThumbnailFromVideo((videoUrl?.absoluteURL)!)`

ビルドして確認してみましょう！
動画選択後にサムネイルが表示されてればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/d1b93c01-60de-7893-f046-b159205a67a1.png)


動画選択の機能は以上です。

## 動画再生の機能を作成する
ここでやること
1. 動画再生用ボタンを設置する
2. ViewControllerに接続する
3. 動画再生の関数を実装する

### 1.動画再生用ボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画再生用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に対してcenter
こんな感じです(急に色付きでスミマセン👾)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/855e21a2-0f61-6fa6-3101-98d65713843b.png)

### 2.ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/5f714941-067f-3ce7-698a-0de716898ef0.png)

### 3.動画再生の関数を実装する
動画を再生するにあたって、再生プレーヤーが必要となります。
Appleが提供している**AVKit**フレームワークに、
ビデオコンテンツを再生するためのインターフェイスが用意されているのでこれを使用していきます。

```swift:VideoUploaderViewController.swift
import UIKit
import Photos
//ここを追加
import AVKit
```
```swift:VideoUploaderViewController.swift
    private let imagePickerController = UIImagePickerController()
    //ここを追加
    private let playerViewController = AVPlayerViewController()
```

次に、動画再生を行う`playVideo(from url: URL)`を実装します。

```swift:VideoUploaderViewController.swift

    private func playVideo(from url: URL) {
        //プレイヤーに受けとったurlをセット
        let player = AVPlayer(url: url)
　　　　　//先ほど初期化したplayerViewControllerのプレイヤーに上記のプレイヤーをセット
        playerViewController.player = player
　　　　　//playerViewControllerの表示・再生
        self.present(playerViewController, animated: true) {
            print("playing video")
            self.playerViewController.player!.play()
        }
    }
```
最後に再生ボタンを押された後に`playVideo(from url: URL)`を呼び出します。

```swift:VideoUploaderViewController.swift
    @IBAction func didTapPlayButton(_ sender: Any) {
        //選択された動画の絶対パスがオプショナル(nilの可能性がある)ので
　　　　　//guard(railsでいうunless)でパスがnilなら早期リターンにしてる
        guard let url = videoUrl?.absoluteURL else { return }
        playVideo(from: url) }
```

ここまで実装できたら、ビルドして再生ボタンを押して確認してみましょう！

動画再生の機能は以上です。

## 動画アップロードの機能を作成する
ここでやること
1. 動画アップロード用のボタンを設置する
2. ViewControllerに接続する
3. 動画アップロードの関数を実装する

### 1.動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に右端対してconstantを71ポイント
こんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/b14cb0ec-4edb-baf8-aa53-27a99fad9179.png)

### 2.ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/7f909b39-b09d-d6b4-b765-3d66861c5d0c.png)

### 3.動画アップロードの関数を実装する
動画のアップロードを行う`uploadVideo()`を実装します。
今回アップロードするのは動画のみです。
アップロードに必要な情報は、
・選択された動画のurl
・選択された動画の名前
になります。

```swift:VideoUploaderViewController.swift
    private func uploadVideo() {
　　　　　//urlと名前がなければ早期リターンさせる
        guard
            let videoClipPath = videoUrl?.absoluteURL,
            //urlの最後がファイル名になる
            let videoClipName = videoUrl?.lastPathComponent
        else { print("not found video path or name"); return }
        //バックエンドにリクエストを送る
　　　　　//ここは次の章で実装するので(APIクライアント)エラーのままで大丈夫です。
        API.postData(videoClipPath: videoClipPath, videoClipName: videoClipName)
    }
```

`uploadVideo()`もアップロード用のボタンが押された時に呼び出されるようにしておきます。

```swift:VideoUploaderViewController.swift
    @IBAction func didTapUploadButton(_ sender: Any) { uploadVideo() }
```

この機能は、APIクライアントとバックエンドのAPIを作って完成します。

# 2.[Swift] APIクライアントの実装🚀

SwiftからRailsにデータを送信するための機能を作っていきます。が、
その前に。

## 動画データの扱い方について

一回やったのですが動画変換は**base64**だと重くてツライので、
APIのリクエストヘッダーのContent-Typeに**multipart/form-data**を指定して
動画をAppleの標準の拡張子.MOVのままバックエンドにアップロードできるように実装します。

## ザックリと用語の解説
### base64
base64はバイナリーデータ(今回は動画)を
String(ASCIIテキスト: アスキーと読みます)に変換する方法です。
バイナリーデータをStringに変換できるのでjson形式で扱えて便利です。
有名な変換方法なので詳しい話はこちらをどうぞ！
[base64ってなんぞ？？理解のために実装してみた
](https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4)
弱点は、上でも書いた通りめちゃくちゃに重たいです。
理由はASCIIテキストに変換していく中でデータサイズが33%増加することです。
例えば、1枚66KBのサンタを変換するとこんな感じ
(ちなみに文字列はまだまだ続く。
base64は100MB未満のデータに適していると言われているのでこのサンタはちょろい方)
[👇🏻base64エンコーダー](https://lab.syncer.jp/Tool/Base64-encode/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/0d49d30f-e3a3-87d9-3210-15519e603445.png)
こんな感じなので動画を全部文字列で扱ったらbase64⇄動画の変換は地獄です。
(1分くらいの動画をbase64でコンソールに出力したらXcodeが固まった)

ということで今回は不採用👻

### multipart/form-data
HTML4から導入された方法で、複合型のコンテンツをMIME Typeを指定することで形式を変えずに送信することができます。(textだったりjpegだったりいろんな形式のデータを一緒に送信することが可能ということ)
HTTPリクエストのヘッダー部分で指定する**Content-type**の一種です。
(Content-typeとは「このリクエストの中身はこんな形式のデータが入ってますよ〜」って宣言。)
詳しくはこちら。[[フロントエンド] multipart/form-dataを理解してみよう](https://www.yoheim.net/blog.php?q=20171201)
今回送信するデータは**video/quicktime**(.MOVファイル)だけですが、拡張性があるのでmultipart/form-dataを指定しています。


## ザックリした流れ
1. Alamofireのインストール
2. APIClient.swiftの作成
3. POSTリクエストを実装
4. http通信の許可

## Alamofireのインストール
APIクライアントを簡単に実装できるライブラリです。**CocoaPods**でインストールします。
導入方法はこちらが詳しいので参考にしてください。(【Swift】CocoaPods導入手順)[https://qiita.com/ShinokiRyosei/items/3090290cb72434852460]
今回`pod 'Alamofire', '~> 4.7.2'`を指定してください。
インストール後、Xcodeを閉じて
プロジェクトディレクトリ以下に出てくる**.xcworkspace**ファイルで開き直したら完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/fb3eb5bd-c05a-0105-ba7d-b63fc7486312.png)

## APIClient.swiftの作成
Xcodeが開けたら、左側に表示されるナビゲーターからプロジェクト名のディレクトリを指定してください。
👇この状態
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/0c18133e-3b55-5c9b-fcef-e4d003f80030.png)
左下の＋ボタン > File... > Swift File の順で、**APIClient.swift**というファイルを作ってください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/be8ee018-2101-aa4f-9c9a-0ca81d3db37b.png)
ナビゲーターの**プロジェクト名のディレクトリ以下**にファイルが追加されていたら完了です。

## POSTリクエストを実装
Alamofireを使って、APIClient.swiftにVideoのPOSTリクエストを書いていきます。
まずはAlamofireをインポートします。

```swift:APIClient.swift
import Alamofire
```

APIクライアントを書いていきます。
ここでやりたいのは、
- Content-type: multipart/form-dataでリクエストを作成
- 動画データの追加
- 送信(エラーハンドリング)
です。

```swift:APIClient.swift
struct API {
    static let baseUrlStr = "http://localhost:3000/api/v1"
    static func postData(videoClipPath: URL, videoClipName: String){
        let requestUrl = URL(string: "\(baseUrlStr)/videos")!
        //multipart/form-dataでデータを送信する
        Alamofire.upload(multipartFormData: { multipartFormData in
            //multipartFormDataオブジェクトに対してデータの追加を行う
            //withNameはrailsのActiveStorage側で保存するときのキーと同じ
　　　　　　  //mimeTypeは、今回.MOVのファイルを扱うので"video/quicktime"を指定
            // mime-type: https://www.tagindex.com/html5/basic/mimetype.html
            multipartFormData.append(videoClipPath, withName: "clip", fileName: videoClipName, mimeType: "video/quicktime")
        }, to: requestUrl) { encodingResult in
            //encodingが成功するとこのハンドラが呼ばれる
            switch encodingResult {
            case.success(let upload, _ ,_):
                print(upload)
                upload
                    .uploadProgress(closure: { (progress) in
                        //進捗率の取得
                        print("Upload Progress: \(progress.fractionCompleted)")
                    })
            case.failure(let error):
                print(error)
            }
        }
    }
}
```
こんな感じでPOSTリクエストの実装は完了です。

## http通信の許可
http通信を許可する
iOS9以降、意図的にドメインを許可する設定をしないとXcodeでhttp通信できなくなりました。
設定しないと以下のようなエラーを吐きます。。

```
The resource could not be loaded because the App Transport Security policy requires the use of a secure connection.
```
今回、rails側のlocalhostを叩きたいのでこの設定が必要になります。
この記事がわかりやすいので設定してみてください🙏🏻
[【swift】XcodeでiOSアプリのhttp通信を許可する方法](https://fukatsu.tech/permit-http-ios)
[おまけ]
ここではバックエンドやフロントエンドでCORSを指定する時でいう
ワイルドカードの指定と同じようなことをやっているので(全ドメインを許可)
本番環境で必要になる場合は(herokuデプロイとか)**Exception Domain**で特定のドメインのみを許可してください。
[iOS9でHTTP通信ができない時の解決法](https://qiita.com/akatsuki174/items/176886ac9f695e2f3d29)

以上でAPIクライアントの実装は終わりです！


# 3.[Rails]APIサーバーの実装🚀
## ザックリした流れ
1. プロジェクトの作成
2. モデルの作成
3. コントローラーの作成
4. ルーティングの作成

## プロジェクトの作成
環境構築は割愛🙏🏻
1. ターミナルを開いて`$ cd /path/to/プロジェクトを作成したいディレクトリ`で移動します
2. `$ rails _5.2.3_ new video_uploader_server --api --skip-test`を実行します(アプリ名はvideo_uploader_serverを好きな名前に変更してください)

| オプション       | 内容       |
|:-----------------|:-----------------|
| `_5.2.3_`| railsのバージョン指定(6以降だと色々面倒なので今回はこちら)|
| --api| APIモード|
| --skip-test| 今回テスト書きません。書く方は抜いてください|

その他optionは[rails newの書き方について徹底解説！](https://www.pikawaka.com/rails/create_app#オプションを確認しよう)を確認してみてください。

## モデルの作成
モデルを作成します。
今回必要なのは
- Videoモデル
- ActiveStrangeで使用するモデル
です。

### ActiveRecordのVideoモデルの作成
1. `$ cd アプリ名`作成したプロジェクトに移動
2. `$ rails g model video`でVideoモデルを作ります(gはgenerateのgです。今回カラム使わないのでこれだけで大丈夫です。)

ActiveStorageの設定を行うのでマイグレーションはまだ行わなくて大丈夫です！
続きをどうぞ！

### ActiveStorageの設定
今回扱うのは動画(.MOV)なのでActiveStorageを使って保存していきます。
[Active Storage の概要](https://railsguides.jp/active_storage_overview.html)に詳しく解説されているので参考にしてください。
1. `$ rails active_storage:install`
ログはこんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/68f705fa-647b-423d-0b08-ec05dbe8ba79.png)
2. `$ rails db:migrate`でデータベースを作ります
3. お好きなエディタでプロジェクトファイルを開きます
4. `video.rb`を開いて編集します

Videoモデルに紐付ける動画ファイルをclipという名前でActiveStorageから呼び出せるように指定します。

```ruby:video.rb
class Video < ApplicationRecord
    #ここを追加
    has_one_attached :clip
end
```
これはActiveStoregeで使用するモデルとVideoモデルのリレーションを定義しています。
Videoモデルにカラムを追加しなくてもVideoモデルに動画を保存しているかのように
ActiveStorageで動画を保存することができるようになります。

## コントローラーの作成
1. `$ rails g controller api/v1/videos`でコントローラーを作成します
v1はバージョン1という意味です。
このように階層を分けてAPIのバージョンを管理するプロジェクトが多いです。
今回は簡単シンプルな構成で機能追加も想定していないので無くても大丈夫です。

2. **videos_controller.rb**を編集します

```ruby:videos_controller.rb
class Api::V1::VideosController < ApplicationController
    # videoの保存
     def create
        video = Video.save(video_params)
        if video.save
            render json: { status: 'ok' }
        else
            render json: { status: 'ng' }
        end
    end
    # videoの取得
    def fetch_latest_video
        # とりあえず最後に保存したもの一件だけを表示
        video = Video.last.clip
        # ActiveStorageで保存したビデオのurlをjsonで返す
        url = url_for(video)
        render json: { url: url }
    end
    
    private 
    def video_params
        params.permit(:clip) #clipはActiveStorageに保存する時のキー
    end
end

``` 

## ルーティングの作成
controllerの階層を考慮した上でvideos_controllerのルーティングが設定できれば問題ないです。

```ruby:routes.rb
Rails.application.routes.draw do
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  namespace 'api' do
    namespace 'v1' do
      # 今回CRUDの機能はcreateのみ使用
      resources :videos, only: :create do
        # idを必要としないパスを生成したいのでcollectionでラップ
        collection do
          #最新のvideoを一件取得するためのパスを/videos/fetch_latest_videoとする定義
         get :fetch_latest_video
        end
      end
    end
  end
end
```

これでルーティングの実装も終わりです。
Xcodeでアプリをビルドして、railsもサーバーを起動してアップロードの確認をしてください。

[ちなみに]
swiftからのリクエストは、rails側のコンソールでこんな感じで受け取ってるのを確認できます。

```
Parameters: {"data"=>"testVideo", "clip"=>#<ActionDispatch::Http::UploadedFile:0x00007f982a2e9c70 @tempfile=#<Tempfile:/var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV>, @original_filename="34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV", @content_type="video/quicktime", @headers="Content-Disposition: form-data; name=\"clip\"; filename=\"34153FC2-F496-4B63-B0EC-D005AA1BC8DB.MOV\"\r\nContent-Type: video/quicktime\r\n">}

```
ターミナルで
`open /var/folders/rk/n8_pgb7x3j18qzjq1g4_4j380000gn/T/RackMultipart20191204-82817-n4evni.MOV` で送られてきた動画の再生ができたりします。

# 4.[Swift]動画プレイヤーの実装🚀
### ザックリした流れ
1. 最新の動画再生用ボタンを設置する
2. ViewControllerに接続する
3. 最新の動画をフェッチするリクエストを実装する
4. 最新の動画を再生する

## 動画アップロード用のボタンを設置する
選択ボタンの時と同じようにMain.storyboardに動画アップロード用のボタンを設置してください。
・横幅は文字の長さ。高さは文字の大きさを２４ポイント
・縦の座標は再生ボタンから42ポイント下
・横の座標は画面に右端対してcenter
こんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/3c3c5318-1479-09d3-20cb-637160fe8f7a.png)

## ViewControllerに接続する
こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233386/90607738-c481-8338-5894-61dc9339ee69.png)

## 最新の動画をフェッチするリクエストを実装する

**APIClient.swift**に以下を追加してください。

```swift:APIClient.swift
    //completionを使うことで呼び出し側でリクエストと動画再生を同期的に扱えるようにしてる
    static func fetchLatestVideoUrl(completion: @escaping (URL) -> ()) {
        //レスポンスの型
        struct FetchResult: Codable {
            let url: String
        }
        let requestUrl = URL(string: "\(baseUrlStr)/videos/fetch_latest_video")!
        //今回パラメーターは特に必要ないので[:](空)で！
        Alamofire.request(requestUrl, method: .get, parameters: [:])
            .responseJSON { response in
                switch response.result {
                case .success:
                    print("Success!")
                    //レスポンスを定義したFetchResultに変換する
                    guard
                        let data = response.data,
                        let result = try? JSONDecoder().decode(FetchResult.self, from: data),
                        //取得できたFetchResultオブジェクトのurl(String🥶)からURLを生成
                        let fetchedUrl = URL(string: result.url)
                    else { return }
                    //取得できたURLをクロージャーに渡す
                    completion(fetchedUrl)
                case .failure:
                    print("Failure!")
                }
        }
    }
```


## 最新の動画を再生する
最新の動画を再生するための関数`playUploadedLatestVideo()`を実装します。

```swift:VideoUploaderViewController.swift
    private func playUploadedLatestVideo() {
        //バックエンドからファイルのurlを返してもらう(http://localhost:3000で始まるもの)
　　　　　//先ほどのcompletionはここの{}(クロージャ)。この中でurlを受け取る
        API.fetchLatestVideoUrl() { url in
            //このurlを使ってプレビューと同様にplayVideo(from url: URL)でビデオを開く
            self.playVideo(from: url)
        }
    }
```
最後にボタンが押されたタイミングで`playUploadedLatestVideo()`を呼び出さすように記述して完成です！

```swift:VideoUploaderViewController.swift
    @IBAction func didTapLatestVideoButton(_ sender: Any) { playUploadedLatestVideo() }
```

# あとがき

お疲れ様でした！
今回は最低限の機能実装でした。
エラーハンドリングしてアラート出したり、一覧ページを作ったり、
削除機能つけたりお好みでいろいろ追加してみてください！

あとそもそもアプリをストアにあげるとかですね！

気になったこととかもっといいやり方あるよとかご指摘は
お気軽にバシバシください🙏🏻

読んでくださりありがとうございました🌻

# ソースコード

[Swift API Client](https://github.com/MoeOnose/VideoUploader)
[Rails API Server](https://github.com/MoeOnose/video_uploader_server)