Help us understand the problem. What is going on with this article?

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

まえがき

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

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

👇こんなの作ります
image.png
機能は、動画選択・プレビュー・アップロード・最新の動画をフェッチして再生です。

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

お勧めしたい人

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

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

ようこそ〜〜〜🍺

学べること

(めっちゃ長いので、お急ぎの方はここから遷移してください🎅🏻)

Swift

Ruby on Rails

その他

使用技術

サーバーサイド(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を使って確実にデプロイする方法【決定版】

いざ実装🚀🚀

全体のザックリした流れ

  1. Swiftで動画アップローダーを作る
  2. SwiftでAPIクライアントを作る
  3. RailsでAPIサーバーを作る
  4. Swiftで動画プレイヤーを作る

~ 完成 ~

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

ザックリした流れ

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

プロジェクトの作成

  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
  2. シュミレーターを起動したら真っ白な画面が出ますがメニューに戻り、写真アプリを開きます
  3. macのfinderから好きな動画を選択します(なければスマホなどからmacに送ってください)
  4. シュミレーターにドラッグアンドドロップで追加します

こんな感じで入ればOK!
image.png

今回使用するファイルの説明

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

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

  • 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を作成する

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

storyboardは自動生成なので、まずはViewController.swiftをリネームていきます。
Xcodeの左側にあるナビゲーターからプロジェクターナビゲーター
(左上のファイルアイコンを押すと出てきます。ファイルの一覧です)を選択し、
ViewControllerをクリックして名前をVideoUploaderViewController.swiftに変更してください。
image.png

ファイル内のclass名も変更します。
image.png

次に、セットで使用するstoryboardに紐づいているViewController名を変更します。
左側のナビゲーターからMain.storyboardを選択します。ここにはViewControllerの白いパーツがあるだけかと思います。
この黄色いアイコンを選択してください。
image.png

右側のインスペクターで、Identity InspectorCustom Classを変更します。
ここを先ほどのVideoUploaderViewController.swiftに変更すると接続が完了します。
image.png

2.Main.storyboardに動画選択用のボタンを追加する

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

Libraryを開いてUIButtonと検索してください。
image.png

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

width/height
widthを設定します。
Ctrを押しながらボタンを選択、ボタン内で選択をやめるとこのようなメニューが出てきます。
image.png
ここでwidthを選択してください。
image.png
赤くなっているのは無視してください。(ちゃんと制約が指定できたら青くなります!)
制約部分を選択すると右側にAttributes Inspectorが開きます。
ここでConstant80にしてください。
image.png

同じようにして、heightのConstantを80で指定してください。
image.png

座標
次にボタンの座標を決めていきます。
画面のトップやボトムから距離を決めたり、画面の真ん中で指定したり方法は色々あります。
今回はトップからの距離と左からの距離を指定します。
先ほどと同じように、Ctrを押しながらボタンを選択、
今度はボタンの外までカーソルを引っ張り選択を解除します。
以下のメニューが出てくるのでTop Space to Safe Areaを選択してください。
image.png
これもConstantを548に変更してください。

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

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

3.ユーザーにカメラロールの使用許可をとる実装を追加する

カメラアプリをインストールした時にこのようなモーダルに遭遇することがあると思います。
image.png
ユーザーから、フォトライブラリにアクセスする許可を取らないと、
動画を選択することができないのでこれを実装していきます。

まず、Info.plistでフォトライブラリを使用する利用目的を設定します。
アプリ名のディレクトリ以下のInfo.plistを開いてください。(テスト用のディレクトリにもあるので注意してください)
Information Property Listの横にある+ボタンを選択して、Privacy - Photo Library Usage Descriptionを入力してください。
行が追加されたら、Valueの部分には利用目的を記述します。
image.png

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

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

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

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

VideoUploaderViewController.swift
import UIKit
import Photos

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

VideoUploaderViewController.swift
    private func confirmPhotoLibraryAuthentication() {

    }

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

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

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関数の下に追加してください。

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)
        //アラートに上記の2つのアクションを追加
        alert.addAction(settingAction)
        alert.addAction(closeAction)
        //アラートを表示させる
        self.present(alert, animated: true, completion: nil)
    }

ここまで書けたら、画面が読み込まれた際に呼び出されるviewDidLoad()から
confirmPhotoLibraryAuthenticationStatus()を呼び出してみましょう。

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

実際に起動して、動作を確認してみましょう。
Info.plistで指定した利用目的はここに出てきます!
image.png

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

image.png

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

4.ボタンをViewControllerに接続する

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

まずVideoUploaderViewController.swiftと先ほどstoryboardに配置したボタンを接続していきます。
Main.storyboardを開いてください。
optionを押しながら、VideoUploaderViewController.swiftを開きます。
左側にMain.storyboard、右側にVideoUploaderViewController.swiftが開けていればOKです。
image.png

storyboardでCtrを押しながらボタンを選択して、ViewcontrollerのviewDidLoadの上まで
カーソルを引っ張ってください。
選択をやめると、このようなメニューが出てくるかと思います。
image.png
以下のようにConnectionActionに変更して、
NamedidTapSelectButtonと入力してConnectを押してください。
image.png
ConnectionActionを指定することで
ボタンをタップされた後に呼び出される関数を自動生成することができます。
image.png

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

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

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

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

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

VideoUploaderViewController.swift
class VideoUploaderViewController: UIViewController{

    let imagePickerController = UIImagePickerController()
    //略

}

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

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()を呼び出すことで
ボタンがタップされた時にフォトライブラリを開き動画を選択することができるようになります。

VideoUploaderViewController.swift
   //選択ボタンがタップされた時に呼び出される
    @IBAction func didTapSelectButton(_ sender: Any) {
        selectVideo()
    }

ビルドして確認してみましょう。動画を選択できるようになったと思います。
image.png

6.選択された動画のサムネイルをViewControllerに表示する

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

Main.storyboardにサムネイルを表示するためのUIImageViewを置く

Main.storyboardを開いてください。
今回はサムネイル(画像)を表示することが目的です。画像を表示するにはUIImageViewを使用します。
UIButtonの時と同じように、LibraryからUIImageViewを検索して追加します。
高さはConstantを250ポイントに指定してください。
横幅は画面いっぱいに指定してください。
画面いっぱいにするには、Ctrを押しながらUIImageViewを選択してカーソルをUIImageViewの外側まで持っていきます。
ここで選択を解除すると以下のメニューが出てきます。
image.png
Equal Widthsを選択すると画面に対して横幅がいっぱいになります。

次に横軸の座標を指定します。今回は画面に対して中央にします。
同様にメニューを出してCenter Horizontally in Safe Areaを指定してください。
このように真ん中にUIImageViewが移動すると思います。
image.png
最後に縦軸の座標を指定します。今回は動画選択のボタンのトップよりUIImageViewのボトムが54ポイント上になるように指定します。
Ctrを押しながら、UIImageViewを選択してください。カーソルは選択ボタンまで持っていってメニューを表示させます。
ここでVertical Spaceを選択してください。
image.png
Constantは54ポイントを指定してください。

これでUIImageViewが置けました。
image.png

UIImageViewをViewControllerに接続する

選択ボタンの時と同じようにMain.storyboardを開いて、隣にVideoUploaderViewcontroller.swiftを開いてください。
(optionを押しながらVideoUploaderViewcontroller.swiftを選択してください)
Ctrを押しながら、UIImageViewをVideoUploaderViewcontroller.swiftに繋いでください。
今度はConnectionOutletのままで、Connectしてください。
image.png
以下のコードが生成されたら接続は完了です。
image.png

ViewControllerをUIImagePickerControllerDelegateとUINavigationControllerDelegateに批准させる

ここが一番わかりづらいと思います🥶
そもそもDelegate(デリゲート)とはSwiftでよく使われる考え方で、ここら辺の記事がわかりやすいです。
【swift】イラストで分かる!具体的なDelegateの使い方。

簡単にいうと、あるクラスが別のクラスに処理をまかせる(委譲する)ことです。
なんで批准という表現が使われるかという話は、例え話がわかりやすいです。
今回扱うデリゲートやプロトコルは、所謂条約のような決まり事です。
必ず守らなければならないルールがあったり、出来ることが増えたりします。
またそこには加盟する国々(ViewControllerなど)がいます。
この関係を国々が条約に批准するというように、
ViewControllerもデリゲートやプロトコルに批准すると考えると理解しやすいです!(個人談)

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

実装方法は、まずクラスを定義している部分にUIViewControllerと同じように必要なDelegateを記述します。
image.png

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

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

これで批准完了です!

imagePickerControllerが閉じられる時に呼び出される関数を実装する

まず、選択された動画のurlを保持するための変数を定義します。

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

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

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という関数を実装します。

VideoUploaderViewController.swift
    private func generateThumbnailFromVideo(_ url: URL) -> UIImage? {
        //以下の3行で縦動画から画像を取り出しても横向きの画像にならないようにしてる
        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

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

動画再生の機能を作成する

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

1.動画再生用ボタンを設置する

選択ボタンの時と同じようにMain.storyboardに動画再生用のボタンを設置してください。
・横幅と高さは選択ボタンと一緒でconstantを80ポイント
・縦の座標は選択ボタンのcenterと同じ
・横の座標は画面に対してcenter
こんな感じです(急に色付きでスミマセン👾)
image.png

2.ViewControllerに接続する

こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
image.png

3.動画再生の関数を実装する

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

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

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

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)を呼び出します。

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

2.ViewControllerに接続する

こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
image.png

3.動画アップロードの関数を実装する

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

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()もアップロード用のボタンが押された時に呼び出されるようにしておきます。

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ってなんぞ??理解のために実装してみた

弱点は、上でも書いた通りめちゃくちゃに重たいです。
理由はASCIIテキストに変換していく中でデータサイズが33%増加することです。
例えば、1枚66KBのサンタを変換するとこんな感じ
(ちなみに文字列はまだまだ続く。
base64は100MB未満のデータに適していると言われているのでこのサンタはちょろい方)
👇🏻base64エンコーダー
image.png
こんな感じなので動画を全部文字列で扱ったらbase64⇄動画の変換は地獄です。
(1分くらいの動画をbase64でコンソールに出力したらXcodeが固まった)

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

multipart/form-data

HTML4から導入された方法で、複合型のコンテンツをMIME Typeを指定することで形式を変えずに送信することができます。(textだったりjpegだったりいろんな形式のデータを一緒に送信することが可能ということ)
HTTPリクエストのヘッダー部分で指定するContent-typeの一種です。
(Content-typeとは「このリクエストの中身はこんな形式のデータが入ってますよ〜」って宣言。)
詳しくはこちら。[フロントエンド] multipart/form-dataを理解してみよう
今回送信するデータは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

APIClient.swiftの作成

Xcodeが開けたら、左側に表示されるナビゲーターからプロジェクト名のディレクトリを指定してください。
👇この状態
image.png
左下の+ボタン > File... > Swift File の順で、APIClient.swiftというファイルを作ってください。
image.png
ナビゲーターのプロジェクト名のディレクトリ以下にファイルが追加されていたら完了です。

POSTリクエストを実装

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

APIClient.swift
import Alamofire

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

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通信を許可する方法
[おまけ]
ここではバックエンドやフロントエンドでCORSを指定する時でいう
ワイルドカードの指定と同じようなことをやっているので(全ドメインを許可)
本番環境で必要になる場合は(herokuデプロイとか)Exception Domainで特定のドメインのみを許可してください。
iOS9でHTTP通信ができない時の解決法

以上で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の書き方について徹底解説!を確認してみてください。

モデルの作成

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

ActiveRecordのVideoモデルの作成

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

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

ActiveStorageの設定

今回扱うのは動画(.MOV)なのでActiveStorageを使って保存していきます。
Active Storage の概要に詳しく解説されているので参考にしてください。
1. $ rails active_storage:install
ログはこんな感じになります
image.png
2. $ rails db:migrateでデータベースを作ります
3. お好きなエディタでプロジェクトファイルを開きます
4. video.rbを開いて編集します

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

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を編集します

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のルーティングが設定できれば問題ないです。

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に動画アップロード用のボタンを設置してください。
・横幅は文字の長さ。高さは文字の大きさを24ポイント
・縦の座標は再生ボタンから42ポイント下
・横の座標は画面に右端対してcenter
こんな感じです。
image.png

ViewControllerに接続する

こちらも選択ボタンと同じようにVideoUploaderViewControllerに接続してください。
以下の関数ができればOKです!
image.png

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

APIClient.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()を実装します。

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()を呼び出さすように記述して完成です!

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

あとがき

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

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

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

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

ソースコード

Swift API Client
Rails API Server

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした