Kekeの日記

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

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

f:id:bobchan1915:20180904035814j:plain

はじめに

やはり社会人になると他の人と一緒にチームを組んで開発することがほとんどだと思います。

しかし、個人で開発していると、オレオレ流のコードを書いていまい、自己満足なコードになってしまいます。 そのような書き方、設計思想に慣れてしまうと社会人になったときに痛い目に合うのでOSSを作っている優秀な人たちのコードのみて学ぶ必要があると感じました 。

1. OSSの選び方

基準としては以下のようなものがあります。

  • 短く分けて読めて、数回でコードを見切れるプロジェクトの規模
  • 実際に実装するケースが多いようなUIを持っていること
  • できるならswift4.0、妥協してswift3.0
  • 開発は盛んであること

また、今回は以下の記事で上がっているものから選定しました。

medium.mybridge.co

2. 実際に選ぶ

そして今回選んだOSSがこちらです。

github.com

Youtubeのクローンになっています。

3. インストールする

以下のようにインストールしてXcodeで開きます。

git clone git@github.com:aslanyanhaik/youtube-iOS.git

そして依存関係をインストールします。

4. 勉強方法のすすめ

以下のような方法で挑戦します。

  • UI構築がどのようになっているのかを確認する
  • 使用されているデザインパターンをみて見る
  • swiftの言語の書き方で学べることがあれば学んでみる
  • などなど

5. まずビルドする

まず、ビルドしてみると以下のような画面が見えます。 f:id:bobchan1915:20180904023600p:plain

画像が大きいですが、まあ基本的なものは見れるのでよいとしましょう。

iPhone 8 plus から iPhone 8にする直りました。

6. 勉強する

UIレイアウト

例えば先ほどのホーム画面は以下のようになっています。

f:id:bobchan1915:20180904023600p:plain

そしてStoryBoardでは以下の通りです。

f:id:bobchan1915:20180904024023p:plain

ここからわかるのは、

  • 赤い部分はNavigationViewでできていて
  • 他はTabelViewで定義している

ということです。

ぱっとみ、これくらいは想定通りです。

他にものぞいてみましょう。

アカウント設定画面は以下のようになっています。

f:id:bobchan1915:20180904024523p:plain

ストーリーボードでは以下のようになっています。

f:id:bobchan1915:20180904024605p:plain

また、お気に入りチャンネルは以下の通りです。

f:id:bobchan1915:20180904024634p:plain

f:id:bobchan1915:20180904024643p:plain

実際の具体的なコードはあとで見ていきます。

設計

以下のようなディレクトリわけになっていました。

YouTube
├── AppDelegate.swift
├── Assets.xcassets
├── Base.lproj
├── Extensions
├── Info.plist
├── LauchVIew.xib
├── Model
├── Supporting\ views
└── ViewControllers

今度から真似てみようと思います。

コードから学ぶ

本記事では以下のHomeViewControllerについて学ぼうと思います。

f:id:bobchan1915:20180904023600p:plain

また、一つでも画面を押すと以下のようになっていて、動画が再生されるようになっています。

f:id:bobchan1915:20180904032521p:plain

HomeVC.swiftは以下のようになっています。

//  MIT License

//  Copyright (c) 2017 Haik Aslanyan

//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:

//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.

//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.

import UIKit

class HomeVC: UIViewController, UITableViewDelegate, UITableViewDataSource, UIScrollViewDelegate  {
    
    //MARK: - Properties
    @IBOutlet weak var tableView: UITableView!
    var videos = [Video]()
    var lastContentOffset: CGFloat = 0.0
    
    //MARK: Methods
    func customization() {
        self.tableView.contentInset = UIEdgeInsetsMake(50, 0, 30, 0)
        self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(50, 0, 30, 0)
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 300
    }
    
    func fetchData() {
        Video.fetchVideos { [weak self] response in
            guard let weakSelf = self else {
                return
            }
            weakSelf.videos = response
            weakSelf.videos.shuffle()
            weakSelf.tableView.reloadData()
        }
    }
    
    //MARK: Delegates
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.videos.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "VideoCell") as! VideoCell
        cell.set(video: self.videos[indexPath.row])
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        NotificationCenter.default.post(name: NSNotification.Name("open"), object: nil)
    }
    
    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)
        }
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        self.lastContentOffset = scrollView.contentOffset.y;
    }
    
    //MARK: -  ViewController Lifecylce
    override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
        self.fetchData()
    }
}

//TableView Custom Classes
class VideoCell: UITableViewCell {
    
    @IBOutlet weak var videoThumbnail: UIImageView!
    @IBOutlet weak var durationLabel: UILabel!
    @IBOutlet weak var channelPic: UIImageView!
    @IBOutlet weak var videoTitle: UILabel!
    @IBOutlet weak var videoDescription: UILabel!
    
    func customization()  {
        self.channelPic.layer.cornerRadius = 24
        self.channelPic.clipsToBounds  = true
        self.durationLabel.layer.borderWidth = 0.5
        self.durationLabel.layer.borderColor = UIColor.white.cgColor
        self.durationLabel.sizeToFit()
    }
    
    func set(video: Video)  {
        self.videoThumbnail.image = video.thumbnail
        self.durationLabel.text = " \(video.duration.secondsToFormattedString()) "
        self.durationLabel.layer.borderColor = UIColor.lightGray.cgColor
        self.durationLabel.layer.borderWidth = 1.0
        self.channelPic.image = video.channel.image
        self.videoTitle.text = video.title
        self.videoDescription.text = "\(video.channel.name)\(video.views)"
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        self.videoThumbnail.image = UIImage.init(named: "emptyTumbnail")
        self.durationLabel.text = nil
        self.channelPic.image = nil
        self.videoTitle.text = nil
        self.videoDescription.text = nil
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.customization()
    }
}

そこまで長くありません。

解説プラス自分がまだアプリエンジニアとして新米なところがあるので勉強しようと思います。

まず、普通のコードであるということを言っておきます。

DataSourceなども一緒に書かれていて、extentionを使ってい分ければいいのになと思いました。

また

class VideoCell: UITableViewCell {
     ...
}

のように同じファイルにセルを定義しています。

僕だったらファイルを分けるのにな、というような感じです。

MVVMの方法は以下の本に詳しく載っていました。

WEB+DB PRESS Vol.106

WEB+DB PRESS Vol.106

  • 作者: 成田元輝,杉浦颯太,小和瀬塁,山中大輔,末田正樹,藤野真聡,竹馬光太郎,桑原仁雄,倉長拓海,牧大輔,秋山卓巳,前田雅央,星北斗,末永恭正,久保田祐史,池田拓司,はまちや2,竹原,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2018/08/24
  • メディア: 単行本
  • この商品を含むブログを見る

ここから内容に入ります。

コードの内容

properties

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

//MARK: - Properties
    @IBOutlet weak var tableView: UITableView! // -----(1)
    var videos = [Video]()
    var lastContentOffset: CGFloat = 0.0

(1)

ここでは以下のような構成なので、このようになっています。

f:id:bobchan1915:20180904030454p:plain

あとのプロパティはそこまで重要ではありません。

viewDidLoad

Viewのライフサイクルの中でもっとも大事な項目といっても過言ではありません。

79行目からは以下のようになっています。

override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
        self.fetchData()
    }

つまり、このメソッドを追えばいいわけです。 これ以降は、メソッドの呼び出しがあったとしても、節を分けて書くようなことはしません。

customization()

関数は以下の通りです。

func customization() {
        self.tableView.contentInset = UIEdgeInsetsMake(50, 0, 30, 0)
        self.tableView.scrollIndicatorInsets = UIEdgeInsetsMake(50, 0, 30, 0)
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.estimatedRowHeight = 300
    }

すべてTableViewの設定(カスタマイズ)をしています。

  • contentInset
  • scrollIndicatorInsets
  • rowHeigth
  • estimatedRowHeight

詳しいことは以下の記事でご覧ください。

qiita.com

fetchData()

以下のようなものです。

func fetchData() {
        Video.fetchVideos { [weak self] response in
            guard let weakSelf = self else {
                return
            }
            weakSelf.videos = response
            weakSelf.videos.shuffle()
            weakSelf.tableView.reloadData()
        }
    }

VideoクラスのfetchVideosメソッドを使っています。

model/Video.swiftの中には以下のようになっていました。

 class func fetchVideo(completion: @escaping ((Video) -> Void)) {
        let video = Video.init(title: "Big Buck Bunny", channelName: "Blender Foundation") // ---- (1)
        video.videoLink = URL.init(string: "http://sample-videos.com/video/mp4/360/big_buck_bunny_360p_10mb.mp4")! // ----(2)
        let suggestedVideo1 = SuggestedVideo.init(title: "What Does Jared Kushner Believe", channelName: "Nerdwriter1")// ---- (3)
        let suggestedVideo2 = SuggestedVideo.init(title: "Moore's Law Is Ending. So, What's Next", channelName: "Seeker") 
        let suggestedVideo3 = SuggestedVideo.init(title: "What Bill Gates is afraid of", channelName: "Vox") 
        let suggestedVideo4 = SuggestedVideo.init(title: "Why Can't America Have a Grown-Up Healthcare Conversation", channelName: "vlogbrothers")
        let suggestedVideo5 = SuggestedVideo.init(title: "TensorFlow Basics - Deep Learning with Neural Networks p. 2", channelName: "sentdex") 
        let items = [suggestedVideo1, suggestedVideo2, suggestedVideo3, suggestedVideo4, suggestedVideo5] // ----- (4)
        video.suggestedVideos = items // ---- (5)
        completion(video) // ---- (6)
    }

ここで知ったのですが、class funcはオーバーライドできるのに対して、static funcはオーバーライドできないことを知りました。

また、@escapingを知らなかったのですが、以下の記事で理解しました。

qiita.com

引数としてクロージャを受け取る時にattributeとして使用します。

使い道としては、

  • クロージャがスコープ外で強参照をされるとき
  • クロージャを非同期で実行する時

に必要です。

そして関数に以下のように渡す必要があります。

{ [weak self] in
   ...
}

クロージャを知らない人は以下のようなものをクロージャと呼びます。

{ (引数) -> 戻り値の型 in
    処理
 }

コードに戻りますが

まず(1)で動画の初期化をしています。

そして(2)で、動画のリンクを設定しています。

(3)では、動画をみているときに出てくるサジェスト動画を設定しています。

また、(4)では、それをitem配列にして(5)で、Videoクラスの

var suggestedVideos = [SuggestedVideo]()

に代入します。

そして(6)で引数として渡ってきたコールバック関数を定義します。

そのコープバック関数は何かというと

{ [weak self] response in
            guard let weakSelf = self else {
                return
            }
            weakSelf.videos = response
            weakSelf.videos.shuffle()
            weakSelf.tableView.reloadData()
        }

のようなものです。

ここでのweakSelfとはHomeVCクラスを指しています。

weakSelf.tableView.reloadData()ではテーブルビューを実際に設定しています。

そして終わりです。

学んだことと課題

仕様以外で学んだことは以下の通りです。

@expectedとweak

ここらへんはあまり理解できていません。

非同期でやりたいから?とぐらいしか思うことができませんでした。

viewDidLoadを簡潔にまとめよう

このコードは以下の短さです。

override func viewDidLoad() {
        super.viewDidLoad()
        self.customization()
        self.fetchData()
    }

次回

次回、このシリーズのエントリーを書くときは

アカウント周りをやろうと思います。

f:id:bobchan1915:20180904024523p:plain