Keke の日記

エンジニア、読書なんでも

人生は短い

ã8åãã®ç»åæ¤ç´¢çµæ

 

皆が知ってる通り、人生は短い。私が幼いとき、私はよくこのことについて考えていた。

 

本当に人生は短いのか、それとも永遠に生きれないことについて文句をつけているだけなのか。もし、 10 倍も人生が長かったら同じことを思うだろうか。

 

私はこの問いに答えが見つからないので、考えるのをやめた。そして、自分に子供ができた。そのことが私に答えを与えてくれた。実際に人生は短いのだ。

 

子供を持つことで、私は連続的な時間を離散的な時間に変換することを学んだ。 2 歳の子供と過ごせるのは 52 週間だ。クリスマスマジック ( 子供がサンタを信じている状態 )が3〜10 歳までしか続かないのなら、たった 8 回しか子供が味わうことを見れないのだ。時間のような連続量に対して何が多く、少ないかを言うことは難しいが、 8 回は「たくさん」というには少ない。仮に、どんなに人生が長い対しても、 8 個のピーナッツや、 8 冊しかない棚の中から読む本を選ばないといけない状況を考えると、かなり制限されているように感じる。

 

やっと人生が短いことが分かった。ただ、それを知ることが何か人生を変えるのか。

 

私の場合はあった。特に「 X をするには人生は短すぎた」ということが大きな影響を及ぼした。これは「何かをするには短い」というスピーチのフレーズを言いたいわけではない。迷惑の類義語でもない。もし、何かをするには人生が短いと感じるならば、それをできるならば排除するべきだ。

 

私は自分は何が人生にとって短いかを自問するとき、「デタラメ」という言葉が頭に思い浮かぶ。「デタラメ」とは何かにしては人生が短すぎることを定義のようなものである。他にも特有の意味がある。それには何か嘘がある。それは経験のジャンクフードであるのだ。

 

もし「デタラメ」である何かに使っているか自問すると、あなたはすでに答えを知っている。必要のないミーティング、的外れの議論、官僚主義、態度、他の人のミスを補っている、渋滞、中毒性のある達成感のない娯楽などである。

 



 

動かなくても、小さなスケールでそれを実現できる。「デタラメ」に使う時間は、雇用主によって変わる。もっと大きな (または小さな ) 組織は、すでに深く浸かっている。もし他の地位や賃金を犠牲にして「デタラメ」を排除することに優先的になれば、よりあなたの時間を無駄にはしない雇用主に出会える。

 

あなたがフリーランサーなら、顧客それぞれのレベルで行うことができる。解雇したり、迷惑な顧客を排除すれば、収入が減る以上に、あなたの時間を守ることができる。

 



 



 



 



 

大切なことを見極める方法は、それが将来も気になるかを自問することだ。偽物は普通、大切に見えるピークがある。これがあなたへのトリックである。カーブの小さいところはエリアが狭いが、ピンのように意識にジャブを打ってくるのである。

 

大切なことは、他の人がいう「大切」と同じである必要はない。友達とコーヒーを飲むのは大切だ。それが時間の無駄だとはのちに感じることはない。

 

小さな子供を持ったことで素晴らしいことは、大切なものに時間を使うようになることだ ( それは子供なのだが ) 。あなたが携帯をみているとき「一緒にあそぼ」といって、袖を掴んでくる。これは「デタラメ」を最小化するオプションなのだ。

 



 



 

人が片方を多く持っていて、もう片方を少なく持っているものは 2 つ思い当たる。それよりも多く得て、それを噛みしめよ。どちらもここでは意味がある。

 

どう生きるかは、人生の長さに影響をする。多くの人はもっとうまくできるはずだ。私も含めて。

 

時間を幾分、遅くすることは可能だ。私はいい方法がある。子供が助けてくれる。小さな子供を持つと、気付かざる瞬間が完璧なほどたくさん見つかる。

 

これは、あなたの経験から絞り出す助けにはならない。だから私は母のことを悲しむのは、彼女が亡くなったからではなくて、一緒にできたたくさんのことを考えるからだ。私の長男は 7 歳になる。 3 歳の彼が愛おしいが、私は少なくとも後悔はない。私たちは父として 3 歳の彼と最高の時間を過ごした。

 

「デタラメ」を容赦無く切り捨てよ。大切なことをすることを待つな、好きな時間を噛み締めよ。人生が短いときはこれをするべきだ。

 

 原文: 

Life is Short

 

YoutubeのiOS アプリ (クローン) のコードを読む会 AccountVC 編

はじめに

前回は以下の記事で HomeViewController について学習しました。

今回は以下のような AccountのView を見ていこうと思います。

f:id:bobchan1915:20180904024523p:plain

StoryBoard を確認する

以下のような対応関係にあります。

f:id:bobchan1915:20180904024523p:plain

f:id:bobchan1915:20180904024605p:plain

非常にシンプルなのがわかります。

ViewController を見ていく

以下のような swift ファイルになっていました。

import UIKit

class AccountVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
    //MARK: - Properties
    @IBOutlet weak var tableView: UITableView!
    let menuTitles = ["History", "My Videos", "Notifications", "Watch Later"]
    var items = 5
    var user = User.init(name: "Loading", profilePic: UIImage(), backgroundImage: UIImage(), playlists: [Playlist]())
    var lastContentOffset: CGFloat = 0.0
    
    //MARK: Methods

    func customization() {
        self.tableView.contentInset = UIEdgeInsetsMake(50, 0, 0, 0)
        self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(50, 0, 0, 0)
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 300
        User.fetchData { [weak self] response in
            guard let weakSelf = self else {
                return
            }
            weakSelf.user = response
            weakSelf.items += response.playlists.count
            weakSelf.tableView.reloadData()
        }
    }
    
    // MARK: Delegates
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch indexPath.row {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Header", for: indexPath) as! AccountHeaderCell
            cell.name.text = self.user.name
            cell.profilePic.image = self.user.profilePic
            cell.backgroundImage.image = self.user.backgroundImage
            return cell
        case 1...4:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Menu", for: indexPath) as! AccountMenuCell
            cell.menuTitles.text = self.menuTitles[indexPath.row - 1]
            cell.menuIcon.image = UIImage.init(named: self.menuTitles[indexPath.row - 1])
           return cell
        case 5...self.items:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Playlists", for: indexPath) as! AccountPlaylistCell
            cell.pic.image = self.user.playlists[indexPath.row - 5].pic
            cell.title.text = self.user.playlists[indexPath.row - 5].title
            cell.numberOfVideos.text = "\(self.user.playlists[indexPath.row - 5].numberOfVideos) videos"
            return cell
        default:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Menu", for: indexPath) as! AccountMenuCell
            return cell
        }
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if (self.lastContentOffset > scrollView.contentOffset.y) {
            NotificationCenter.default.post(name: NSNotification.Name("hide"), object: false)
        } else {
            NotificationCenter.default.post(name: NSNotification.Name("hide"), object: true)
        }
    }
    
    //MARK: -  ViewController Lifecylce
    override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
    }
}

class AccountHeaderCell: UITableViewCell {
    
    @IBOutlet weak var name: UILabel!
    @IBOutlet weak var profilePic: UIImageView!
    @IBOutlet weak var backgroundImage: UIImageView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.profilePic.layer.cornerRadius = 25
        self.profilePic.clipsToBounds = true
    }
}

class AccountMenuCell: UITableViewCell {
    
    @IBOutlet weak var menuIcon: UIImageView!
    @IBOutlet weak var menuTitles: UILabel!
    
}

class AccountPlaylistCell: UITableViewCell {
    
    @IBOutlet weak var pic: UIImageView!
    @IBOutlet weak var title: UILabel!
    @IBOutlet weak var numberOfVideos: UILabel!
    
    override func awakeFromNib() {
        self.pic.layer.cornerRadius = 5
        self.pic.clipsToBounds = true
    }
}

ひとつひとつみていく

ViewController のプロパティ

以下のようなプロパティをもっています。

 //MARK: - Properties
    @IBOutlet weak var tableView: UITableView!
    let menuTitles = ["History", "My Videos", "Notifications", "Watch Later"]
    var items = 5
    var user = User.init(name: "Loading", profilePic: UIImage(), backgroundImage: UIImage(), playlists: [Playlist]())
    var lastContentOffset: CGFloat = 0.0

tableView を持っていたり、メニューのタイトルををもっていたり、ユーザーの情報を持っていたりします。

viewDidLoad ()

以下のようになっています。

//MARK: -  ViewController Lifecylce
    override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
    }

一つしかメソッドを実行していません。

そのめそっどは 以下のようになっています。

 func customization() {
        self.tableView.contentInset = UIEdgeInsetsMake(50, 0, 0, 0)
        self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(50, 0, 0, 0)
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 300
        User.fetchData { [weak self] response in
            guard let weakSelf = self else {
                return
            }
            weakSelf.user = response
            weakSelf.items += response.playlists.count
            weakSelf.tableView.reloadData()
        }
    }

このようにtableViewを設定していることがわかります。

delegates

// MARK: Delegates
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch indexPath.row {
        case 0:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Header", for: indexPath) as! AccountHeaderCell
            cell.name.text = self.user.name
            cell.profilePic.image = self.user.profilePic
            cell.backgroundImage.image = self.user.backgroundImage
            return cell
        case 1...4:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Menu", for: indexPath) as! AccountMenuCell
            cell.menuTitles.text = self.menuTitles[indexPath.row - 1]
            cell.menuIcon.image = UIImage.init(named: self.menuTitles[indexPath.row - 1])
           return cell
        case 5...self.items:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Playlists", for: indexPath) as! AccountPlaylistCell
            cell.pic.image = self.user.playlists[indexPath.row - 5].pic
            cell.title.text = self.user.playlists[indexPath.row - 5].title
            cell.numberOfVideos.text = "\(self.user.playlists[indexPath.row - 5].numberOfVideos) videos"
            return cell
        default:
            let cell = tableView.dequeueReusableCell(withIdentifier: "Menu", for: indexPath) as! AccountMenuCell
            return cell
        }
    }

このように行ごとによってセルを切り変えていることがわかります。

このようににて実現しているのです。

まとめ

  • iOS 初心者なので、「どのようにして設定などを定義しているのだろうと思っていたので勉強になった
  • TableView はセルを繰り返すものというよりは、行ごとにレイアウトしたいものに対して使うものっという認識になった

OAuth2.0 の仕組みとクライアントの作成

はじめに

今回は OAuth2.0 の認可方式を取る GitHub API を使って、クライアントの作成を行いたいと思います。

動機

自分がよく使うからにつきます。

Github で、その 1 日のコミット数を Slack に投げたり、フル活躍しています。

せっかく使っているのなら、記事として、言葉として昇華させたいと思ったので本記事を執筆しようと思いました。

1. OAuth2.0 とは

1.1 ロール

このシステムでは、 4 つのロールがいます。

ロール 役割
リソースオーナー だいたいはユーザーである。
リソースサーバー アカウントをを持っているサーバーのことであり、アプリケーションをことを指すことが多い。
クライアント リソースを要求していて、ユーザーの代わりに取ってくるものである。一般的にはアプリケーションである。
認可サーバー (注意 1) 認可をするためだけのサーバー。リソースと認可処理を分けたいなどのニーズからこのようなサーバーを持っていることが多い。

注意 1: リソースサーバーが認可も同様に行う場合があって、そのような場合は兼ねているのでリソースサーバーとまとめる。

1.2 ターム

システムを知る、開発していくっと言う中で出くわすいくつかの専門用語を解説します。

ターム 説明
認可コード 短時間で失効してしまう、アクセストークンを取得のために使用するコード、トークンのこと
アクセストークン 認可コードの有効性を検証されるともらえるリソースサーバにアクセスするためのトークン

1.3 ユースケース

一体、どんなときにこのようなシステムをわざわざ使うの ? って思う人もいるかもしれません。

しかし、実際には世の中にありふれています。

StackOverFlow

ja.stackoverflow.com

CircleCI

circleci.com

などデベロッパー向けのもあれば、たとえば

Instagram

www.instagram.com

TweetDeck

tweetdeck.twitter.com

のように私たちの生活にはかなり浸透しているものもそうです。

2. 認可のフロー

OAuth2.0, Authorization Code Flow のスクリーンショットを参考に解説してきます。

https://camo.qiitausercontent.com/cbceb0f0e391aeeb9220c484838d0c13e730c75d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3130363034342f64393131396632312d373336642d643565642d393634642d3330363861663066636465392e706e67

たとえば TwitterやFacebook など SNS をまとめて表示してくれるサービスをアプリ XYZ としましょう。

もちろんアプリ XYZはTwitterやFacebook そのものではないので投稿をはじめとするコンテンツを取得しなければなりません。

そのような中で、「連携」と一般的に言われているような機能があるとします。

たとえば twelog だと以下のようなページです。

https://syncer.jp/twitter-how-to-setting-connect-application/medias/twitter-how-to-setting-connect-application-info-01.png

このときに「サービス ABC と連携をする」押したとしましょう。

ここで、サービス ABC は、あなたがすでにアカウントを登録しているサービスのことです。 アプリ XYZ は、サービス ABC の認可サーバーに、認可エンドポイントと認可リクエストを送信します。

すると例えば以下のようなサービス ABC の認可画面に飛ばされます。

http://imgc.appbank.net/c/wp-content/uploads/2014/04/Howto-Twitter-App-Renkei-1.jpg

サービス ABC のユーザー名やパスワードを答えると、その情報は認可決定エンドポイントに送信されます。

認可に成功すると、非常に短い間だけ有効な認可コードして、アプリ XYZ に返します。

そのコードをもってして、トークンエンドポイントに送信すると、アクセストークンを発行される。

そのアクセストークンを使って、リソースサーバの API を叩くことができる。 API を受信したリソースサーバーは、トークンの有効性を検証して、有効な場合のみリソースを返す。

以上のような流れになっています。

3. シェルスクリプトで作成してみる

あとあとの命名法にも関連してくるので、アプリケーション名はSGITとします。

今回は以下のような要件のクライアントを作成しようと思っています。

  • Github の自分のプライベートリポジトリの情報を表示する

もちろん Github のプライベートリポジトリはユーザの許可なしに見ることはできません。

なので、今回はチャレンジしようと思います。

また、先ほどのロールと対応づけると以下のようになります。

| ロール | 今回作るアプリ | |:--- :|:--- :| | リソースオーナー | ユーザー ( 自分 ) | | リソースサーバー | GitHubAPI サーバー | | クライアント | 今回の CLI ツール | | 認可サーバー | GitHubAPI サーバー |

最大の注意点なのですが、私はfish shellを使っています。

3.0 スクリプトファイルを作成

以下のコマンドでスクリプトファイルを作成します。

公式チュートリアルは以下の通りです。

developer.github.com

まず、ファイル名を決めます。

set -x FILE_NAME "hoge"

そしてファイルを作成したのちに、権限を与えます。

echo "#!/usr/local/bin/fish" > $FILE_NAME
chmod +x $FILE_NAME 

何も起きませんが、試しに実行してみてください。

./hoge

何もエラーがでなければ次に進んでください。

3.1 ユーザーに認証するかを確認する (オプショナル )

図にもあったように、最終的にはアクセストークンを使って CLI ツールがリソースを取得することになります。 つまり、アクセストークンを無くすことがあれば、再度取得する必要があるのです。

すでに連携している場合は「連携していますが」のようなフィードバックを、または「初期化を望みますか」といったようなことを返すのがユーザーエクスピリアンス (UX) のためには必要と思われます。

この機能は UX のためにあるので実装するかは任意ですので、飛ばしてもらってもかまいません。

今回はアクセストークンは環境変数として保存します。

他のアプリケーションがパソコン内にうじゃうじゃいることを考えると、以下のように命名します。

SGIN_ACCESS_TOKEN

つまり、この値がセットされていなかったら実行して、セットされていなかったら、今のところ実行されないようにします。

-nが空文字であるかをチェックするので

if test -n $SGIN_ACCESS_TOKEN
    echo "Will create token"
else
    echo "Already has one"
end

のようにします。

実行すると以下のように返ってきます。

./shell/sgin
Will create token

また、直接空文字であることも確認します。

echo $SGIN_ACCESS_TOKEN

3.2 認可エンドポイントにアクセスして、リダイレクトを行う

まず github の場合、以下の認可エンドポイントにアクセスしなければなりません。

https://api.github.com/authorizations

--userオプションをつけてユーザー名を入力します。

3.2.1 必須パラメータ

  • note: なんのためのトークンを発行するつもりなのかを明記します。

3.2.2 オプショナルパラメーター

使いそうな主なパラメータを紹介します。

  • scopes: どの部分まで権限があるかを確認する

全て知りたければ以下のリンクを参照してください。

developer.github.com

3.2.3 例

以下のようなリクエストを送ると

{
  "scopes": [
    "public_repo"
  ],
  "note": "admin script"
}

以下のように返ってきます。

{
  "id": 1,
  "url": "https://api.github.com/authorizations/1",
  "scopes": [
    "public_repo"
  ],
  "token": "abcdefgh12345678",
  "token_last_eight": "12345678",
  "hashed_token": "25f94a2a5c7fbaf499c665bc73d67c1c87e496da8985131633ee0a95819db2e8",
  "app": {
    "url": "http://my-github-app.com",
    "name": "my github app",
    "client_id": "abcde12345fghij67890"
  },
  "note": "optional note",
  "note_url": "http://optional/note/url",
  "updated_at": "2011-09-06T20:39:23Z",
  "created_at": "2011-09-06T17:26:27Z",
  "fingerprint": ""
}

どのようなscopeがあるのかは以下の一覧で確認することができます。

developer.github.com

本題に戻って実際に送ってみます。 実際には Basic 認証をしているようなので-u オプションで指定します。

すると二段階認証をしているので、以下のようなメッセージが返ってきました。

{
  "message": "Must specify two-factor authentication OTP code.",
  "documentation_url": "https://developer.github.com/v3/auth#working-with-two-factor-authentication"
}

ドキュメントにいってみて何をするべきか確認します。

f:id:bobchan1915:20180903051904p:plain

翻訳すると

For users with two-factor authentication enabled, Basic Authentication requires an extra step.

もし二段階認証をしている場合は、追加のステップが必要です。

When you attempt to authenticate with Basic Authentication, the server will respond with a 401 and an X-GitHub-OTP: required;:2fa-type header. This indicates that a two-factor authentication code is needed 。

もし Basic 認証による認証を試行すると、サーバーは 401 を返して、そのレスポンスにはX-GitHub-OTP: required; :2fa-typeのようなヘッダーが付いている。これは二段階認証コードが必要なことを意味しています。

...

あとはまとめると

  • 2 段階認証コードをX-GitHub-OTPヘッダーに合わせて送る必要がある
  • Github さんは二段階認証コードがすぐ失効するものなので、 Authorizations API を使ってアクセストークンを作ってそのトークンを使うことを推奨する

とのことです。

まず最初のものを行ってみると

curl --silent -u KeisukeYamashita KeisukeYamashita https://api.github.com/authorizations -d '{"scopes":["public_repo"],"note":"Google Issues to GH"}' -H "X-GitHub-OTP: 936402" 

HASH 関数は一方向関数なのでhashed_tokenを消す意味はそこまでないのですが、消しちゃいました。

{
  "id": 216091729,
  "url": "https://api.github.com/authorizations/216091729",
  "app": {
    "name": "Google Issues to GH",
    "url": "https://developer.github.com/v3/oauth_authorizations/",
    "client_id": "00000000000000000000"
  },
  "token": "TOKEN",
  "hashed_token": "HASED_TOKEN",
  "token_last_eight": "5f7466a4",
  "note": "Google Issues to GH",
  "note_url": null,
  "created_at": "2018-09-02T20:39:56Z",
  "updated_at": "2018-09-02T20:39:56Z",
  "scopes": [
    "public_repo"
  ],
  "fingerprint": null
}

と返ってきました。

GUI でも確認することができます。

f:id:bobchan1915:20180903054249p:plain

やっとアクセストークンを取得することができました。

3.4 アクセストークンを使って API を叩く

さきほど取得したアクセストークンを使って API を叩きます。

curl https://api.github.com/user/repos  -H 'Accept: application/vnd.github.v3+json' -H "Authorization: token $TOKEN"

すると取得できます。ここでは、レスポンスは表示されません。

まとめ

二段階認証をしてからは、端末が変わるごとに二段階認証コードを入れないといけなくなったので不便。

しかし、一度アクセストークンを取得できると、やりたい放題。

参考文献

qiita.com

qiita.com

YoutubeのiOS アプリ (クローン) のコードを読む会 NavigationController 編

はじめに

これまで Youtube のクローンを教材に勉強してきて、

HomeVC

AccountVC

をやってきました。特に変則的なことはなかったのですが、 Youtube といったら使いやすい NavigationVC だと思います。

今回はそれを解説したいと思います。以下のようなものが対応しています。

f:id:bobchan1915:20180904045608p:plain

NavigationViewController の使い方復習

まず、 NavigationViewController の使い方を復習しようと思います。

参考にしたのは以下の記事です。

i-app-tec.com

UINavigationController - UIKit | Apple Developer Documentation

1. どのようなものか

以下のような階層的な UI の構築には良いと思います。

https://docs-assets.developer.apple.com/published/83ef757907/navigation_interface_2x_8f059f7f-2e2f-4c86-8468-7402b7b3cfe0.png

2. 階層イメージ

以下の画像が参考になります。

https://docs-assets.developer.apple.com/published/83ef757907/NavigationViews_2x_e69e98a2-aaac-477e-9e33-92e633e29cc7.png

3. 今回はどこに使われているか

たとえば今回の場合は、

f:id:bobchan1915:20180904045652p:plain

のStoryBoard です。

そのような中で、まず

  • MainVC を開く

そして、次の ViewControllerへNavigate してくれるようなものです。

StoryBoard を確認する

全体の StoryBoard は以下のようになっています。

f:id:bobchan1915:20180904045652p:plain

NavigationVC を見る

以下のようなコードになっています。

import UIKit

class NavVC: UINavigationController, PlayerVCDelegate  {

    //MARK: Properties
    @IBOutlet var playerView: PlayerView!
    @IBOutlet var searchView: SearchView!
    @IBOutlet var settingsView: SettingsView!
    let titleLabel = UILabel()
    let names = ["Home", "Trending", "Subscriptions", "Account"]
    let hiddenOrigin: CGPoint = {
        let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 10
        let x = -UIScreen.main.bounds.width
        let coordinate = CGPoint.init(x: x, y: y)
        return coordinate
    }()
    let minimizedOrigin: CGPoint = {
        let x = UIScreen.main.bounds.width/2 - 10
        let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 10
        let coordinate = CGPoint.init(x: x, y: y)
        return coordinate
    }()
    let fullScreenOrigin = CGPoint.init(x: 0, y: 0)

    //Methods
    func customization() {
        //NavigationBar buttons
        //Settings Button
        let settingsButton = UIButton.init(type: .system)
        settingsButton.setImage(UIImage.init(named: "navSettings"), for: .normal)
        settingsButton.tintColor = UIColor.white
        settingsButton.addTarget(self, action: #selector(self.showSettings), for: UIControlEvents.touchUpInside)
        self.navigationBar.addSubview(settingsButton)
        settingsButton.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .height, relatedBy: .equal, toItem: settingsButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: settingsButton, attribute: .width, relatedBy: .equal, toItem: settingsButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .centerY, relatedBy: .equal, toItem: settingsButton, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .right, relatedBy: .equal, toItem: settingsButton, attribute: .right, multiplier: 1.0, constant: 10).isActive = true
        //SearchButton
        let searchButton = UIButton.init(type: .system)
        searchButton.setImage(UIImage.init(named: "navSearch"), for: .normal)
        searchButton.tintColor = UIColor.white
        searchButton.addTarget(self, action: #selector(self.showSearch), for: UIControlEvents.touchUpInside)
        self.navigationBar.addSubview(searchButton)
        searchButton.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .height, relatedBy: .equal, toItem: searchButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: searchButton, attribute: .width, relatedBy: .equal, toItem: searchButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .centerY, relatedBy: .equal, toItem: searchButton, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: searchButton, attribute: .right, relatedBy: .equal, toItem: settingsButton, attribute: .left, multiplier: 1.0, constant: -10).isActive = true
        //TitleLabel setup
        self.titleLabel.font = UIFont.systemFont(ofSize: 18)
        self.titleLabel.textColor = UIColor.white
        self.titleLabel.text = self.names[0]
        self.navigationBar.addSubview(self.titleLabel)
        self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .centerY, relatedBy: .equal, toItem: self.titleLabel, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .height, relatedBy: .equal, toItem: self.titleLabel, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .left, relatedBy: .equal, toItem: self.titleLabel, attribute: .left, multiplier: 1.0, constant: -10).isActive = true
        self.titleLabel.widthAnchor.constraint(equalToConstant: 200).isActive = true
        //NavigationBar color and shadow
        self.navigationBar.barTintColor = UIColor.rbg(r: 228, g: 34, b: 24)
        self.navigationBar.setBackgroundImage(UIImage(), for: .default)
        self.navigationBar.shadowImage = UIImage()
        self.navigationItem.hidesBackButton = true
        //SearchView setup
        self.view.addSubview(self.searchView)
        self.searchView.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .top, relatedBy: .equal, toItem: self.searchView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .left, relatedBy: .equal, toItem: self.searchView, attribute: .left, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .right, relatedBy: .equal, toItem: self.searchView, attribute: .right, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.searchView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
        self.searchView.isHidden = true
        //SettingsView setup
        self.view.addSubview(self.settingsView)
        self.settingsView.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .top, relatedBy: .equal, toItem: self.settingsView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .left, relatedBy: .equal, toItem: self.settingsView, attribute: .left, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .right, relatedBy: .equal, toItem: self.settingsView, attribute: .right, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.settingsView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
        self.settingsView.isHidden = true
        //PLayerView setup
        self.playerView.frame = CGRect.init(origin: self.hiddenOrigin, size: UIScreen.main.bounds.size)
        self.playerView.delegate = self
        //NotificaionCenter Setup
        NotificationCenter.default.addObserver(self, selector: #selector(self.changeTitle(notification:)), name: Notification.Name.init(rawValue: "scrollMenu"), object: nil)
    }
    
    @objc func showSearch()  {
        self.searchView.alpha = 0
        self.searchView.isHidden = false
        UIView.animate(withDuration: 0.2, animations: {
            self.searchView.alpha = 1
        }) { _ in
            self.searchView.inputField.becomeFirstResponder()
        }
    }
    
    @objc func showSettings() {
        self.settingsView.isHidden = false
        self.settingsView.tableViewBottomConstraint.constant = 0
        UIView.animate(withDuration: 0.3) { 
            self.settingsView.backgroundView.alpha = 0.5
            self.settingsView.layoutIfNeeded()
        }
    }
    
    @objc func changeTitle(notification: Notification)  {
        if let info = notification.userInfo {
            let userInfo = info as! [String: CGFloat]
            self.titleLabel.text = self.names[Int(round(userInfo["length"]!))]
        }
    }
    
    func animatePlayView(toState: stateOfVC) {
        switch toState {
        case .fullScreen:
            UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 5, options: [.beginFromCurrentState], animations: {
                self.playerView.frame.origin = self.fullScreenOrigin
            })
        case .minimized:
            UIView.animate(withDuration: 0.3, animations: {
                self.playerView.frame.origin = self.minimizedOrigin
            })
        case .hidden:
            UIView.animate(withDuration: 0.3, animations: {
                self.playerView.frame.origin = self.hiddenOrigin
            })
        }
    }
    
    func positionDuringSwipe(scaleFactor: CGFloat) -> CGPoint {
        let width = UIScreen.main.bounds.width * 0.5 * scaleFactor
        let height = width * 9 / 16
        let x = (UIScreen.main.bounds.width - 10) * scaleFactor - width
        let y = (UIScreen.main.bounds.height - 10) * scaleFactor - height
        let coordinate = CGPoint.init(x: x, y: y)
        return coordinate
    }
    
    //MARK: Delegate methods
    func didMinimize() {
        self.animatePlayView(toState: .minimized)
    }
    
    func didmaximize(){
        self.animatePlayView(toState: .fullScreen)
    }
    
    func didEndedSwipe(toState: stateOfVC){
        self.animatePlayView(toState: toState)
    }
    
    func swipeToMinimize(translation: CGFloat, toState: stateOfVC){
        switch toState {
        case .fullScreen:
            self.playerView.frame.origin = self.positionDuringSwipe(scaleFactor: translation)
        case .hidden:
            self.playerView.frame.origin.x = UIScreen.main.bounds.width/2 - abs(translation) - 10
        case .minimized:
            self.playerView.frame.origin = self.positionDuringSwipe(scaleFactor: translation)
        }
    }
    
    //MARK: ViewController lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        if let window = UIApplication.shared.keyWindow {
            window.addSubview(self.playerView)
        }
    }
}

1. Property

以下のようなプロパティを持っています。

//MARK: Properties
    @IBOutlet var playerView: PlayerView!
    @IBOutlet var searchView: SearchView!
    @IBOutlet var settingsView: SettingsView!
    let titleLabel = UILabel()
    let names = ["Home", "Trending", "Subscriptions", "Account"]
    let hiddenOrigin: CGPoint = {
        let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 10
        let x = -UIScreen.main.bounds.width
        let coordinate = CGPoint.init(x: x, y: y)
        return coordinate
    }()
    let minimizedOrigin: CGPoint = {
        let x = UIScreen.main.bounds.width/2 - 10
        let y = UIScreen.main.bounds.height - (UIScreen.main.bounds.width * 9 / 32) - 10
        let coordinate = CGPoint.init(x: x, y: y)
        return coordinate
    }()
    let fullScreenOrigin = CGPoint.init(x: 0, y: 0)

また、これらの

  • playerView
  • searchView
  • settingsView

supporting View/以下に定義されています。

ので、のちのちみます。

2. viewDidLoad ()

ライフサイクルで大事なので、見ていきます。

以下のようになっています。

//MARK: ViewController lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
    }

のようになっています。

customization () は以下のとおりです。

//Methods
    func customization() {
        //NavigationBar buttons
        //Settings Button
        let settingsButton = UIButton.init(type: .system)
        settingsButton.setImage(UIImage.init(named: "navSettings"), for: .normal)
        settingsButton.tintColor = UIColor.white
        settingsButton.addTarget(self, action: #selector(self.showSettings), for: UIControlEvents.touchUpInside)
        self.navigationBar.addSubview(settingsButton)
        settingsButton.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .height, relatedBy: .equal, toItem: settingsButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: settingsButton, attribute: .width, relatedBy: .equal, toItem: settingsButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .centerY, relatedBy: .equal, toItem: settingsButton, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .right, relatedBy: .equal, toItem: settingsButton, attribute: .right, multiplier: 1.0, constant: 10).isActive = true
        //SearchButton
        let searchButton = UIButton.init(type: .system)
        searchButton.setImage(UIImage.init(named: "navSearch"), for: .normal)
        searchButton.tintColor = UIColor.white
        searchButton.addTarget(self, action: #selector(self.showSearch), for: UIControlEvents.touchUpInside)
        self.navigationBar.addSubview(searchButton)
        searchButton.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .height, relatedBy: .equal, toItem: searchButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: searchButton, attribute: .width, relatedBy: .equal, toItem: searchButton, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .centerY, relatedBy: .equal, toItem: searchButton, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: searchButton, attribute: .right, relatedBy: .equal, toItem: settingsButton, attribute: .left, multiplier: 1.0, constant: -10).isActive = true
        //TitleLabel setup
        self.titleLabel.font = UIFont.systemFont(ofSize: 18)
        self.titleLabel.textColor = UIColor.white
        self.titleLabel.text = self.names[0]
        self.navigationBar.addSubview(self.titleLabel)
        self.titleLabel.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .centerY, relatedBy: .equal, toItem: self.titleLabel, attribute: .centerY, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .height, relatedBy: .equal, toItem: self.titleLabel, attribute: .height, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.navigationBar, attribute: .left, relatedBy: .equal, toItem: self.titleLabel, attribute: .left, multiplier: 1.0, constant: -10).isActive = true
        self.titleLabel.widthAnchor.constraint(equalToConstant: 200).isActive = true
        //NavigationBar color and shadow
        self.navigationBar.barTintColor = UIColor.rbg(r: 228, g: 34, b: 24)
        self.navigationBar.setBackgroundImage(UIImage(), for: .default)
        self.navigationBar.shadowImage = UIImage()
        self.navigationItem.hidesBackButton = true
        //SearchView setup
        self.view.addSubview(self.searchView)
        self.searchView.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .top, relatedBy: .equal, toItem: self.searchView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .left, relatedBy: .equal, toItem: self.searchView, attribute: .left, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .right, relatedBy: .equal, toItem: self.searchView, attribute: .right, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.searchView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
        self.searchView.isHidden = true
        //SettingsView setup
        self.view.addSubview(self.settingsView)
        self.settingsView.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .top, relatedBy: .equal, toItem: self.settingsView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .left, relatedBy: .equal, toItem: self.settingsView, attribute: .left, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .right, relatedBy: .equal, toItem: self.settingsView, attribute: .right, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .bottom, relatedBy: .equal, toItem: self.settingsView, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
        self.settingsView.isHidden = true
        //PLayerView setup
        self.playerView.frame = CGRect.init(origin: self.hiddenOrigin, size: UIScreen.main.bounds.size)
        self.playerView.delegate = self
        //NotificaionCenter Setup
        NotificationCenter.default.addObserver(self, selector: #selector(self.changeTitle(notification:)), name: Notification.Name.init(rawValue: "scrollMenu"), object: nil)
    }

非常に長いのですが、やっていることは先ほどの

  • playerView
  • searchView
  • settingsView

の初期化みたいなものです。

また、ボタンであればそれぞれshowSearchなど関数が定義されています。

3. viewDidAppear ()

以下のようになっています。

override func viewDidAppear(_ animated: Bool) {
        super.viewWillAppear(true)
        if let window = UIApplication.shared.keyWindow {
            window.addSubview(self.playerView)
        }
    }

です。

MainVC

MainVCは以下のようになっています。

import UIKit

class MainVC: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    
    //MARK: Properties
    @IBOutlet var tabBarView: TabBarView!
    @IBOutlet weak var collectionView: UICollectionView!
    var views = [UIView]()
    
    //MARK: Methods
    func customization()  {
        self.view.backgroundColor = UIColor.rbg(r: 228, g: 34, b: 24)
        //CollectionView Setup
        self.collectionView.contentInset = UIEdgeInsetsMake(44, 0, 0, 0)
        self.collectionView.frame = CGRect.init(x: 0, y: 0, width: UIScreen.main.bounds.width, height: (self.view.bounds.height))
        //TabbarView setup
        self.view.addSubview(self.tabBarView)
        self.tabBarView.translatesAutoresizingMaskIntoConstraints = false
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .top, relatedBy: .equal, toItem: self.tabBarView, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .left, relatedBy: .equal, toItem: self.tabBarView, attribute: .left, multiplier: 1.0, constant: 0).isActive = true
        let _ = NSLayoutConstraint.init(item: self.view, attribute: .right, relatedBy: .equal, toItem: self.tabBarView, attribute: .right, multiplier: 1.0, constant: 0).isActive = true
        self.tabBarView.heightAnchor.constraint(equalToConstant: 64).isActive = true
        //ViewController init
        let homeVC = self.storyboard?.instantiateViewController(withIdentifier: "HomeVC")
        let trendingVC = self.storyboard?.instantiateViewController(withIdentifier: "TrendingVC")
        let subscriptionsVC = self.storyboard?.instantiateViewController(withIdentifier: "SubscriptionsVC")
        let accountVC = self.storyboard?.instantiateViewController(withIdentifier: "AccountVC")
        let viewControllers = [homeVC, trendingVC, subscriptionsVC, accountVC]
        for vc in viewControllers {
            self.addChildViewController(vc!)
            vc!.didMove(toParentViewController: self)
            vc!.view.frame = CGRect.init(x: 0, y: 0, width: self.view.bounds.width, height: (self.view.bounds.height - 44))
            self.views.append(vc!.view)
        }
        self.collectionView.reloadData()
        //NotificationCenter setup
        NotificationCenter.default.addObserver(self, selector: #selector(self.scrollViews(notification:)), name: Notification.Name.init(rawValue: "didSelectMenu"), object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.hideBar(notification:)), name: NSNotification.Name("hide"), object: nil)
    }
    
    @objc func scrollViews(notification: Notification) {
        if let info = notification.userInfo {
            let userInfo = info as! [String: Int]
            self.collectionView.scrollToItem(at: IndexPath.init(row: userInfo["index"]!, section: 0), at: .centeredHorizontally, animated: true)
        }
    }
    
    @objc func hideBar(notification: NSNotification)  {
        let state = notification.object as! Bool
        self.navigationController?.setNavigationBarHidden(state, animated: true)
    }
    
    //MARK: Delegates
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.views.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
        cell.contentView.addSubview(self.views[indexPath.row])
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize.init(width: self.collectionView.bounds.width, height: (self.collectionView.bounds.height + 22))
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
       let scrollIndex = scrollView.contentOffset.x / self.view.bounds.width
        NotificationCenter.default.post(name: Notification.Name.init(rawValue: "scrollMenu"), object: nil, userInfo: ["length": scrollIndex])
    }
    
    //MARK: ViewController lifecyle
    override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

1. Property

また、以下のようなプロパティをを持っています。

//MARK: Properties
    @IBOutlet var tabBarView: TabBarView!
    @IBOutlet weak var collectionView: UICollectionView!
    var views = [UIView]()
  • tabBarView: タブのメニューを
  • collectionView:
  • views: Navigate するビューの集合

viewDidload ()

これは他のものと一緒なので、省略させていただきます。

3. 各部品

3.1 SearchView

以下のようになっていますが、ビルドしてま動作しないので今回は見ないことにします。

import UIKit

class SearchView: UIView, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    
    //MARK: Properties
    @IBOutlet weak var inputField: UITextField!
    @IBOutlet weak var tableView: UITableView!
    var suggestions = [String]()
    
    //MARK: Methods
    func customization() {
        self.tableView.delegate = self
        self.tableView.dataSource = self
        self.inputField.delegate = self
    }
    
    @IBAction func hideSearchView(_ sender: Any) {
        self.inputField.text = ""
        self.suggestions.removeAll()
        self.tableView.isHidden = true
        self.inputField.resignFirstResponder()
        UIView.animate(withDuration: 0.2, animations: {
            self.alpha = 0
        }) { _ in
            self.isHidden = true
        }
    }
    
    //MARK: Delegates
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.suggestions.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! SearchCell
        cell.resultLabel.text = self.suggestions[indexPath.row]
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.inputField.text = self.suggestions[indexPath.row]
        let cell = tableView.cellForRow(at: indexPath)
        cell?.isSelected = false
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        self.hideSearchView(self)
        return true
    }
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard let text = self.inputField.text else {
            self.suggestions.removeAll()
            self.tableView.isHidden = true
            return true
        }
        let netText = text.addingPercentEncoding(withAllowedCharacters: CharacterSet())!
        let url = URL.init(string: "https://api.bing.com/osjson.aspx?query=\(netText)")!
        let _  = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] (data, response, error) in
            guard let weakSelf = self else {
                return
            }
            if error == nil {
                if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) {
                    let data = json as! [Any]
                    DispatchQueue.main.async {
                        weakSelf.suggestions = data[1] as! [String]
                        if weakSelf.suggestions.count > 0 {
                            weakSelf.tableView.reloadData()
                            weakSelf.tableView.isHidden = false
                        } else {
                            weakSelf.tableView.isHidden = true
                        }
                    }
                }
            }
        }).resume()
        return true
    }
    
    //MARK: View LifeCycle
    override func awakeFromNib() {
        super.awakeFromNib()
        self.customization()
    }
}

class SearchCell: UITableViewCell {
    @IBOutlet weak var resultLabel: UILabel!
}

3.2 SettingView

設定ボタンを押すと以下のように出ます。

f:id:bobchan1915:20180904052124p:plain

下から出てくるものがtableviewとして定義されてあります。

import UIKit

class SettingsView: UIView, UITableViewDelegate, UITableViewDataSource {
    
    //MARK: Properties
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var backgroundView: UIButton!
    @IBOutlet weak var tableViewBottomConstraint: NSLayoutConstraint!
     let items = ["Settings", "Terms & privacy policy", "Send Feedback", "Help", "Switch Account", "Cancel"]

    //MARK: Methods
    func customization() {
        self.tableView.delegate = self
        self.tableView.dataSource = self
        self.backgroundView.alpha = 0
        self.tableViewBottomConstraint.constant = -self.tableView.bounds.height
        self.layoutIfNeeded()
    }
    
    @IBAction func hideSettingsView(_ sender: Any) {
        self.tableViewBottomConstraint.constant = -self.tableView.bounds.height
        UIView.animate(withDuration: 0.3, animations: { 
            self.backgroundView.alpha = 0
            self.layoutIfNeeded()
        }) { _ in
            self.isHidden = true
        }
    }
    
    //MARK: Delegates
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = self.items[indexPath.row]
        cell.imageView?.image = UIImage.init(named: self.items[indexPath.row])
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.hideSettingsView(self)
    }
    
    //MARK: View LifeCycle
    override func awakeFromNib() {
        super.awakeFromNib()
        self.customization()
    }
}

3.3 PlayerView

以下のような窓のやつです。

まだこのレベルはたかそうだと思って、今回はスルーしました。

f:id:bobchan1915:20180904052309p:plain

3.4 TabBarView

タブのビューで、横スクロールすると変わるものを定義しています。

import UIKit

class TabBarView: UIView, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDelegate {
    
    //MARK: Properties
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var whiteBar: UIView!
    @IBOutlet weak var whiteBarLeadingConstraint: NSLayoutConstraint!
    private let tabBarImages = ["home", "trending", "subscriptions", "account"]
    var selectedIndex = 0
    
    //MARK: Methods
    func customization() {
        self.collectionView.delegate = self
        self.collectionView.dataSource = self
        self.backgroundColor = UIColor.rbg(r: 228, g: 34, b: 24)
        NotificationCenter.default.addObserver(self, selector: #selector(self.animateMenu(notification:)), name: Notification.Name.init(rawValue: "scrollMenu"), object: nil)
    }
    
    @objc func animateMenu(notification: Notification) {
        if let info = notification.userInfo {
            let userInfo = info as! [String: CGFloat]
            self.whiteBarLeadingConstraint.constant = self.whiteBar.bounds.width * userInfo["length"]!
            self.selectedIndex = Int(round(userInfo["length"]!))
            self.layoutIfNeeded()
            self.collectionView.reloadData()
        }
    }
    
    //MARK: Delegates
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.tabBarImages.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! TabBarCellCollectionViewCell
        var imageName = self.tabBarImages[indexPath.row]
        if self.selectedIndex == indexPath.row {
            imageName += "Selected"
        }
        cell.icon.image = UIImage.init(named: imageName)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize.init(width: collectionView.bounds.width / 4, height: collectionView.bounds.height)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        if self.selectedIndex != indexPath.row {
            self.selectedIndex = indexPath.row
            NotificationCenter.default.post(name: Notification.Name.init(rawValue: "didSelectMenu"), object: nil, userInfo: ["index": self.selectedIndex])
        }
    }
    
    //MARK: View LifeCycle
    override func awakeFromNib() {
        super.awakeFromNib()
        self.customization()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

//TabBarCell Class
class TabBarCellCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var icon: UIImageView!
}

このような箇所にCollectionViewを使っています。

CollectionView 以下の記事を参考にしました。

www.raywenderlich.com

CollectionView でしていること

** 1.

4. まとめ

今回は学ぶことがかなり大きかったです。

というのも、あまり NavigationViewController を高度にカスタマイズした優良な記事は少ないので非常に勉強になりました。

身近な Machine Learning as a Service! UberのMichelangelo

f:id:bobchan1915:20180816105817j:plain

自己紹介

  • 最近Apache HadoopCassandraApache Kafkafluendを使ってサービス構築をしている。
  • 得意な言語はgolangjavascript
  • 好きなライブラリ、フレームワークはgolang echoVue.js
  • 開発経験分野: サーバーサイド、フロントエンド、ロボット、 iOS アプリ、インフラ
  • 好きなサービスはGCP Cloud BuildSpinnakerKubernetes

本記事の対象者

  • Machine learning as a Service (MLaaS) に興味がある人
  • ビッグデータのインフラ構築に興味があるひと

Uber Michelangelo とは

2015 年半ばから始まったプロジェクト。

Uberが開発している社内機械学習プラットフォームのことである。 主に Uber の交通予測に使われていて、 Uber Eats だとお届け時間を予測したりしている。

f:id:bobchan1915:20180908233446p:plain

またその自動でプロダクションコンテナへデプロイを可能にして、ワークフローをend-2-endなシステムで完結させ、チーム間や組織内で利用できるようにする仕組みである。

Michelangelo 開発のきっかけ

継続的デプロイなどの背景にある原因が機械学習モデルにも同様に見られたからです。

以下のような

  • ビルド
  • デプロイ
  • ツールの多様化 (scikit-learnなど)
  • パイプラインの再利用性
  • ミュータブル環境構築

などの項目では組織体制に問題があった。また、チームごとでも方針も、技術選定も違っていた。結果的にインフラエンジニアが少数いるように、機械学習エンジニア、サイエンティストが少数に限られていてしまっていた。

システムアーキテクチャ

OSS によって構築される。 主に使用されているのは以下のような OSS である。

OSS 名 主な機能
HDFS 分散ファイルシステム
Spark 分散処理フレームワーク
Hive Hadoopを置き換える目的の分散処理フレームワーク
Samza ストリーム処理フレームワーク
Cassandra 分散 NoSQL
その他機械学習 OSS -

前提

一般的なデータ収集基盤などは大まかに以下の手順で構成される。

  1. データを収集
  2. データを加工する
  3. データを永続化する

Michelangelo のアーキテクチャ

1. データレイク

Kafkafluentdによってログデータなどによって、全てのデータが保存している場所である。

一般的には非構造データである。

2. 機械学習フロー

一般的な機械学習エンジン構築と同じである。

2.1 データ管理

オンラインとオフラインパイプラインに分かれている。

オンライン 低レイテンシで処理する。リアルタイム性が求められるようなもの。

オフライン バッジ処理をする。データマートを作成するためなど、要件としてはなににでも使える。

また、組織内で処理したデータを使用するためにデータを管理している。

構築図は以下の通りである。

f:id:bobchan1915:20180801021725p:plain

詳細解説

  • Data Lake: ここはあらゆる構造化、非構造化されたデータが保存されている場所であり、 Uber はHDFSを使用している。
  • Data Prep Job: ここではSpark JobとしてSQLでデータを取り出して分散処理をしている。
  • Hive feature store: ここはHadoopのラッパーであるHiveに保存して、分散処理でCassandraにデータを格納している。
  • Cassandra Feature Store: 組織間でデータを使用するために格納している。読み出しが非常に高速で行うことができる。
  • Outcomes: Spark でラベリングなどをした結果を保存している。

2.2 学習

f:id:bobchan1915:20180908232921p:plain

学習では一般的にはオフラインで行なっている。 モデル設定はハイパーパラメータや、コンピュータリソースなで設定している。 YARNやMesos で動いているトレーニングジョブを設定できる。

2.3 モデル評価

モデルは全て保存され、 Cassandra に格納される。

以下の項目が保存される。

  • 誰が学習させたか
  • 開始、終了時間
  • モデルの設定 (features used, hyper-parameter values, etc.)
  • トレーニングデータとテストデータのリファレンス/
  • Distribution and relative importance of each feature
  • モデルの精度
  • モデルのグラフ (e.g. ROC curve, PR curve, and confusion matrix for a binary classifier)
  • 学習済みのパラメータ Summary statistics for model visualization

2.4 モデルデプロイ

f:id:bobchan1915:20180908233001p:plain

モデルのデプロイはどのように行なっているのかがわからないが、継続的デリバリーを行なっていると考えられる。

2.5 予測に使う

f:id:bobchan1915:20180908233016p:plain

モデルがデプロイされると、 HiveやCassandra などからFeature Storeで取り出されて、予測したり、実際のトラフィックから予測したりする。

2.6 予測をモニタリングする

以下のように予測した結果もどうようにデータベースに格納してモニタリングをする。

f:id:bobchan1915:20180908233103p:plain

2.7 可視化、 API 化

以下のように可視化して、モニタリングをしやすい環境を整えている。

f:id:bobchan1915:20180908233416p:plain

データレイクとは

引用: AWS データレイク



まとめるとなんでもデータを保存している場所。

参考文献

eng.uber.com

ApacheのIncubator プロジェクト Apache Gearpump でストリーム処理をしてみる

f:id:bobchan1915:20180908225908p:plain

Apache Gearpump とは

概要

Apache Gearpump とは、

リアルタイムビッグデータストリームエンジン

です。なお、 Apache Software Foundation Incubator project になっています。

特徴

他のストリーム処理エンジンと違って、 Gearpump はイベント / メッセージ (あとで解説する) ベースである。

他にも特徴としては

  • 非常に高いスループットと低いレイテンシストリーム処理
  • メッセージセマンティクスを設定することができる (at least once, exactly once)
  • アプリケーションのホットなリデプロイ
  • アプリケーションモニタリングのための包括的なダッシュボード
  • Storm アプリケーションの互換性
  • Samoa アプリケーションの互換性
  • 使いやすい API と拡張性の高い API 。
  • 高レベルも低レベル API もサポート

インターフェース

以下のインターフェースが用意されています。

  • JavaAPI
  • ScalaAPI
  • RESTAPI

基本的なコンセプト

MasterとWorker

Gearpump はマスタースレーブアーキテクチャをとります。

すべてのクラスタには一つの Master が一つ以上の Worker を持っています。

役割としては

  • Master: クラスタのリソースをまとめる
  • Worker: マシン単体のリソースを管理する

さらに詳細は以下のように構成になっています。

https://gearpump.apache.org/releases/latest/img/actor_hierarchy.png

Application

Application とは、私たちが分散処理をしたいアプリケーションのことをさします。 バッジ処理を対象とする MapReduce アプリケーションや、ストリーム処理を対象とするストリーム処理アプリケーションがありますが、 Gearpump はストリーム処理アプリケーションをサポートしている。

AppMasterとExecutor

ランタイムの中で、すべてのアプリケーションのインスタンスは一つの AppMaster と、 Executor のリストで構成されます。

AppMasterはApplication 自体のコントロールセンターのようなもので命令します。

これはユーザーとも、 Master とも、 Worker とも、 Executor とも通信し、ジョブと呼ばれる処理を終えることを全うします。

Executor は分散したワーカーの中での並行処理を行うものです。

Application のアップロードフロー

公式ドキュメントでは、 Application Submission Flow と書かれていますがアップロードと訳しています。

https://gearpump.apache.org/releases/latest/img/submit.png

  1. ユーザーがアプリケーションをアップロードすると、 Master は実行できる Worker を探して AppMaster が作れそうなら、 Master が受け取ります。
  2. AppMaster が起動されると、 MasterにWorker を要求します。 Worker 中の Executor が起動するとジョブを始める。
  3. Master にその結果が返される

以下のようなフローになっています。

https://gearpump.apache.org/releases/latest/img/submit2.png

ApMaster は使用可能な Worker を配置して、 Worker のサブプロセスとして AppMaster を起動します。

ストリームトポロジー、プロセッサー、タスク

ストリームアプリケーションでは、 DAG のトポロジーをもつアプリケーションがあり、データプローを定義している。

https://gearpump.apache.org/releases/latest/img/dag.png

ストリームタスクとパーティショナー

タスクとはストリーム処理内の並行処理可能なアトミックな概念である。

ランタイム内では、プロセッサーの中にタスクがいくつかある。

その中で、どのようにデータフローを制御するかを設定することができる。

https://gearpump.apache.org/releases/latest/img/shuffle.png

技術的なハイライト

Gearpump は高いパフォーマンスと、柔軟で、耐障害性の高い、レスポンジブルなストリームプラットフォームである。

どこでも Actor

再度、掲載になりますが、 Actor のヒエラルキーは以下のようになっています。

https://gearpump.apache.org/releases/latest/img/actor_hierarchy.png

アクターモデルを取っていて、マイクロサービスのように他のアクターとは切り離されている凝縮性の高いコンポーネントで構成されています。

アクターをたくさん定義することによって、複雑なタスクをこなすことができる。

Exactly once セマンティクス

厳密にデータは一回だけ確実に届くのが Exactly once なメッセージセマンティクスです。

そして時間計算より、再送信して、未来の時間で再度計算されるようなことはありません。

https://gearpump.apache.org/releases/latest/img/exact.png

Flow コントロール

ビルドインでフローコントロールがサポートされています。

タスク間でメッセージがやりとりされる中で、下流のタスクが詰まってしまわないようにします。

https://gearpump.apache.org/releases/latest/img/flowcontrol.png

ビルドインダッシュボード

バックエンドに RESTAPI を使っと WebUI ダッシュボードが用意されています。

https://gearpump.apache.org/releases/latest/img/dashboard.gif

ビルドイン Apache KafkaとHDFS コネクター

一般的に広くデータ基盤開発で使われている Apache KafkaやHDFS のコネクターがすでにあるので、簡単に導入しやすそうです。

Docker でインストール

コンテナの起動

念のため Docker Image があることを確認します。

docker search gearpump

NAME                                      DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
gearpump/gearpump                         Gearpump docker container for one node clust…   4                                       [OK]
...

そして以下のコマンドでまず gearpump のコンテナを取得します。

docker pull gearpump/gearpump

そして以下のコマンドでコンテナを起動します。

docker run -t -p 8090:8090 --name gearpump gearpump/gearpump

バックグラウンドで起動したいときは-dオプションをつけてください。

ここで https://localhost:8090 にアクセスすると、ダッシュボードを見ることができます。

f:id:bobchan1915:20180908204207p:plain

ちなみに

  • username: admin
  • password: admin

でログインをすることができます。

もちろんアプリケーションは何もできていません。

f:id:bobchan1915:20180908204336p:plain

Dockerfile を読む

以下が Docker コンテナを作るための Dockerfile になります。

# Docker file
FROM errordeveloper/oracle-jre

# Prepare the package
RUN curl --location   --retry 3 --insecure https://github.com/gearpump/gearpump/releases/download/0.7.1/gearpump-2.11-0.7.1.zip -o tmp.zip && unzip -q tmp.zip && rm tmp.zip && chmod +x gearpump-2.11-0.7.1/bin/*

ADD gear.conf gearpump-2.11-0.7.1/conf/gear.conf

EXPOSE 3000

EXPOSE 8090

ENTRYPOINT gearpump-2.11-0.7.1/bin/local -workernum 1 & gearpump-2.11-0.7.1/bin/services

ここで知る必要があるのは以下のADDが書かれている行です。

ADD gear.conf gearpump-2.11-0.7.1/conf/gear.conf

このコマンドだと自分のカレントディレクトリにあるgear.confをコピーをコンテナ内に追加しています。

どうやらgear.confを作って設定ファイルを定義していけば良さそうなことが推測できます。

サンプルアプリケーションをアップロードする

以下のリポジトリに gearpump の公式リポジトリとなるのでクローンしてください。

git clone git@github.com:apache/incubator-gearpump.git

そして、ビルドしてください。

sbt clean assembly packArchiveZip

そして.zipファイルができるので、回答します。

unzip gearpump-2.11-0.8.4.zip

そして CLI ツールで jar ファイルをアップロードします。

bin/gear app -jar examples/wordcount-2.11-0.8.4-assembly.jar org.apache.gearpump.streaming.examples.wordcount.WordCount

設定ファイルgear.confについて

以下のドキュメントで設定について見ることができます。

Configuration - Apache Gearpump (incubating)

Gearpump トポロジーの書き方

今回は Scala で説明します。

object WordCount extends AkkaApp with ArgumentsParser {

  override val options: Array[(String, CLIOption[Any])] = Array.empty

  override def main(akkaConf: Config, args: Array[String]): Unit = {
    val context = ClientContext(akkaConf)
    val app = StreamApp("dsl", context)
    val data = "This is a good start, bingo!! bingo!!"

    //count for each word and output to log
    app.source(data.lines.toList, 1, "source").
      // word => (word, count)
      flatMap(line => line.split("[\\s]+")).map((_, 1)).
      // (word, count1), (word, count2) => (word, count1 + count2)
      groupByKey().sum.log

     context.submit(app).waitUntilFinish()
     context.close()
  }
}

基本的にはAkkaAppをextend することで実行できます。

Sourceがトポロジー内でデータの入力となります。

そして中で処理をしたあと、contextcloseすることによってトポロジーを閉じることができます。

まとめ

  • どの言語でもかけるわけではないので、つらい
  • 基本的な構成はおおまか、どのストリーム処理エンジンと一緒である
  • 調べ不足なだけかもしれないが、耐障害性の根拠が気になる

適宜、追記する予定です。

Swift4.0 でカスタムオペレーターを作成してみる

https://developer.apple.com/swift/images/swift-og.png

はじめに

カスタムオペレーターとは+-の四則演算に加えて新たに自分で演算子を定義できるものです。

特に以下の GPU に関するフレームワークでは

camera --> filter --> renderView

のような演算子が使われています。

なぜ使うのか

一般的に可読性の観点だと考えることができます。

特に関数型プログラムは、パイプライン演算子と呼ばれるような演算子があるおかげで非常に簡潔に書くことができます。

定義の仕方

まずは低い優先度で定義します。

precedencegroup Base {
    associativity: left
    lowerThan: AdditionPrecedence
}

そして infix という、日本語直訳では取り付けっといったような意味をもつものでオペレータを定義します。

infix operator <+> : Base
infix operator <*> : Base

そして、その関数が一体どのような関数であるかを定義します。

以下のような感じです。

public func <+> (lhs: Int, rhs: Int) -> Int {
    print("<+>: called `Base`")

    return lhs + rhs
}

public func <*> (lhs: Int, rhs: Int) -> Int {
    print("<*>: called `Base`")

    return lhs * rhs
}

Playground でも確認することができました。

<+>: called `Base`
<*>: called `Base`
20

構造体やについても同様のことをすることができて、 ObjectiveーC のようにメンバにアクセスすることができます。

ただし注意が必要でinoutをつけることによって参照型を渡す必要があります。

これは GolangやObjective-C でも、ポインタを渡す必要があるのと一緒です。

まずはクラスを定義します。

public class User {
    var name = ""
}

そして演算子を適当に定義します。

infix operator -> : Base

public func -> (lhs: inout User, rhs: String) -> Void {
    lhs.name = rhs
}

そして実際に使用します。

var user = User()
let name = "Test"
user->name
print(user.name)

// => Test

Objective-C との比較

以下のように Objective-C ではメンバにアクセスします。

User *user = [[ User alloc ] init];

tarou->name = @"KeisukeYamashita";

今回のカスタム演算子を使うと

var user = User()
user->"KeisukeYamashita

Swift はオーバーロードを定義することができるので使ってみるといいです。

そのときは明示的にclass funcとキーワードをつけてからやるのがいいと思います。

オーバーロードしないのならstaticをつけるべきです。

エラーハンドル付きカスタム演算子

以下のようにもちろん例外処理もすることができます。

エラー処理をすることを明言するためにthrowsを付け加えます。

func -> (lhs: Int, rhs: Int) throw -> Int {
    if ... {
        // ...
     }
}

そして使うときに

do {
    try 1 -> 2
} catch {
    // ...
}

とすることができます。

結合と優先度

最初に以下のように定義しました。

precedencegroup Base {
    associativity: left
    lowerThan: AdditionPrecedence
}

associativity

数学でいう結合性のことです。 線形代数の初歩ですね。

1 + 2 + 3 が (1 + 2) + 3 と解釈されるのが左結合で、 1 + (2 + 3) と解釈されるのが右結合です。

特に行列演算などは交換可能ではないので、注意が必要です。

lowerThan

これは優先度の話です。

+-×などと比べると優先度が低いと言えます。

このような優先度を定義しているわけです。

演算子のオーバーロード

オーバーライドする方法があります。

オーバーロードとオーバーライドは大きく異なるので注意が必要です。

まとめ

必要になったら定義してよみやすいコードを書いていければと思います。

参考文献

qiita.com

medium.com

www.studiogalago.com

Bye bye GPUImage2, Hello XXX?

読了時間は約 7 分です。

f:id:bobchan1915:20180907143043p:plain

画像引用: https://developer.apple.com/jp/metal/

Bye bye GPUImage2

iOS12 からの GPUImage2 非推奨へ

少し語弊があるので説明すると GPUImage2 自体が非推奨なわけではなくて、 GPUImage2 内部で使われている Open GL (ES) がiOS12 から非推奨になるとのことです。

もう一つ追記すると、非推奨なので元のものをすぐに使えなくなるわけではありません。

Apple 公式ソース (英語): What’s New in iOS - Apple Developer

Apple 公式ソース (日本語): iOS の新機能 - Apple Developer

実際に GPUImage 内でも以下のような Issue が上がっています。

github.com

Why

これは Khronos Group が開発した Vulkan と似たようなシチュエーションです。

実はすでに Open GL やその組み込み版である Open GL (ES) など、オープンソースで、かつ、 GPU によるグラフィック処理をすることができるものはありました。

しかし、近年の GPU をはじめとしたハードウェアの進化は凄まじく、 OpenGL のような抽象化されすぎて、利便性を追い求めたものがオーバーヘッドを生んでしまい、ハードウェアの限界性能を引き出せなくなっていたという現実がありました。

その解決策として、より低レベルの API を提供したグラフィック APIVulkan が出た歴史があります。

同様に Apple MetalもVulkan と競合するフレームワークです。同様の問題があって、オーバーヘッド軽減に関する効果があることから開発されたと推測することができます。

Apple 公式ソース (日本語): iOS の新機能 - Apple Developer

Hello XXX?

では、どのような対応をすればいいのでしょうか。

公式のドキュメントでは Apple 製のグラフィック処理フレームワーク Metal を使うことが推奨されています。

しかし、 Metal を使うとなっても直接低レベルの API をアプリケーションで呼び出すには開発コストや既存の GPUImage2 などのライブラリからの置き換えを考えると無理難題です。

そこで以下のような GPUImage に置き換わるようなライブラリがいくつか開発されているので検討してみようと思います。

  • CoreImage
  • GPUImage3
  • FlexibleImage
  • MetalPetal
  • MetalAcc

詳細を述べる前に、 2018/09/07 時点の簡単な比較表で各プロジェクトの比較をしてみます。

なお、ドキュメントの充実度は主観により、⭐️ 3 つで正則化しています。

リポ名 star 数 fork 数 contributor 数 ドキュメントの充実度 ライセンス
Core Image - - - ⭐️⭐️⭐️ -
BradLarson/GPUImage3 1124 42 4 ⭐️⭐️ BSD 3-Clause
kawoou/FlexibleImage 724 33 1 ⭐️⭐️⭐️ MIT
MetalPetal/MetalPetal 238 31 5 ⭐️⭐️⭐️ MIT
wangjwchn/MetalAcc 213 18 1 ⭐️ MIT

また現時点での必要条件は以下の通りです。記載がないものは-を使っています。

リポ名 swift iOS Xcode OSX tvOS
Core Image - 5.0 以上 9.0 以上 10.11 以上 9.0 以上
BradLarson/GPUImage3 4.0 以上 9.0 以上 9.0 以上 10.11 以上 -
kawoou/FlexibleImage 3.0 以上 8.0 以上 - 10.10 以上 9.0 以上
MetalPetal/MetalPetal - - - - -
wangjwchn/MetalAcc - - - - -

Core Image

Core Image | Apple Developer Documentation

ここでは取り上げませんでしたが、 Core Graphic の後継ライブラリです。

Apple のビルドインの静止画や動画に任意のフィルターをかけるものです。

特徴としては

  • パフォーマンスがいい
  • ビルドインフィルターを使ってパイプラインを構築することができる、その種類が非常に豊富
  • 直接コードを書いて定義することができるか

使えるビルドインフィルターの種類は Core Image Filter Reference で確認することができます。

基本的には Core Image クラスCIImageを使うのでUIImageをキャストする必要があります。逆も然りです。

// UIImage -> CIImage
let ciImage:CIImage? = CIImage(image: uiImage)

// CIImage -> UIImage
let uiImage:UIImage? = UIImage(ciImage: ciImage)

そしてCIFilterを使ってフィルターを定義して

let filter = CIFilter(name: "CIPhotoEffectMono")!

filter.setDefaults()
filter.setValue(ciImage, forKey: kCIInputImageKey)

最後に filter を施します。

let output = filter.outputImage

非常に簡単な印象です。

GPUImage3

github.com

GPUImage フレームワークの第三世代目です。

GPUImageはObjective-C で書かれていて、 GPUImage2 はそれを Swift に書き直したものです。

その GPUImage2 では Open GL (ES) をラッピングしていたましたが、この世代は Metal をラッピングしていて、パフォーマンスの最適化や Metal をベースに作られたフレームワークと高い統合性を示します。

しかしながら、 GPUImage2 の機能を完全にカバーしているわけではなくて、フルカバー終え次第、外部コントリビュータからの IssueやPull Request を受け付けるそうです。

2018/09/07 時点で未実装の機能は以下の通りです。

  • 静止画をキャプチャしてフィルタリングする
  • 動画から画像をキャプチャする
  • 静止画を処理する
  • 動画を再エンコーディングする

ただし、開発が活発な印象があります。

f:id:bobchan1915:20180907153015p:plain

FlexibleImage

github.com

FlexibleImageはMetal を使って静止画のみに対応しているグラフィックプラットフォームです。

特にドキュメントが非常に簡潔で、わかりやすく、examples以下にいろんなプラットフォームでの使用例があるので、静止画ならとっつきやすい印象があります。

一般的なフィルターには以下のようにフィルターをかけます。

/// Generate Example
let image1 = UIImage
    .circle(
        color: UIColor.blue,
        size: CGSize(width: 100, height: 100)
    )!
    
    .adjust()
    .offset(CGPoint(x: 25, y: 0))
    .margin(UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5))
    .padding(UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15))
    .normal(color: UIColor.white)
    .border(color: UIColor.red, lineWidth: 5, radius: 50)
    .image()!
    
    .adjust()
    .background(color: UIColor.darkGray)
    .image()

特に使いやすいイメージがあります。

MetalPetal

github.com

MetalPetalはGPUImage3 と同様に Metal を使ってリアルタイムで動画や静止画を処理するためのフレームワークです。

以下のように設計されています。

  • 使いやすい API
  • パフォーマンスがいいこと
  • 拡張性の高さ
  • Swifty にかける

また、コアなコンポーネントは Apple Core Image Framework に近いです。

しかし、何が Core Image と何がちがうのかというと

  • 頂点とフラグメントの関数を完全にカスタマイズできる
  • MRT をサポートしている
  • 一般的にはパフォーマンスがいい

そうです。しかしながら、ベンチマークを取っていないので注意が必要です。

以下のようにフィルターを施します。

処理の対象はMTIImageクラスのでキャストする必要があります。

入力画像としては Core ImageかCore Graphic クラスである必要があります。

// CIImage -> MTIImage
let imageFromCIImage = MTIImage(ciImage: ciImage)

そしてフィルターを定義します。

let filter = MTISaturationFilter()
filter.saturation = 0
filter.inputImage = inputImage

そして以下のようにして、出力を取得できます。

let outputImage = filter.outputImage

MetalAcc

github.com

こちらも Swift で書かれた MetalのGPU ベースの処理ライブラリで、 GPUImage にインスパイアされて作成されています。

しかしながら、ドキュメントが整っておらず、機能も静止画のフィルターに限られているという印象です。

一般的な使用方法は以下の通りです。

let accImage = AccImage()
        
accImage.Input(inimage)
        
accImage.AddProcessor(AccBrightnessFilter())
        
accImage.Processing()
        
let outimage = accImage.Output()

総括

パフォーマンスについてはベンチマークを取っていないので今回は比較できません。

あとは比較できるとすると機能面、設計ではないでしょうか。

機能については、以下の表に比較します。

なお、ライブラリやフレームワークそれ単体で実現できない機能も❌としています。 (たとえば動画に対応していなくても、スクショなど静止画にするという対応を前処理として取るなど)

また、 GPUImage3 については、 GPUImage2 で実装されていた項目について実装されるので をつけています。

Blur

FlexibleImageは単に Blur しか書かれていないので❓をつけています。

機能 Core Image GPUImage3 FlexibleImage MetalPetal MetalAcc
静止画に対応
動画に対応  
BoxBlur  
DiscBlur  
GaussianBlur  
SingleComponentGaussianBlur
iOSBlur
MaskedVariableBlur
MedianFilter
MotionBlur
NoiseReduction
ZoomBlur
Lens (Hexagonal Bokeh) Blur

まとめ

iOS12 から Open GL (ES) を使ったライブラリは非推奨となるので Warning が出るようになるので組織としても技術的負債を抱えるのは辛いと思います。

この記事が少しでも参考になれば嬉しいです。

今度、ベンチマークを取れればいいなと思います。

読んでいただきありがとうございました !

参考文献

Core Image について qiita.com

Core ImageとMetal について qiita.com