YoutubeのiOSアプリ(クローン)のコードを読む会 HomeView編
はじめに
やはり社会人になると他の人と一緒にチームを組んで開発することがほとんどだと思います。
しかし、個人で開発していると、オレオレ流のコードを書いていまい、自己満足なコードになってしまいます。 そのような書き方、設計思想に慣れてしまうと社会人になったときに痛い目に合うのでOSSを作っている優秀な人たちのコードのみて学ぶ必要があると感じました 。
1. OSSの選び方
基準としては以下のようなものがあります。
- 短く分けて読めて、数回でコードを見切れるプロジェクトの規模
- 実際に実装するケースが多いようなUIを持っていること
- できるならswift4.0、妥協してswift3.0
- 開発は盛んであること
また、今回は以下の記事で上がっているものから選定しました。
2. 実際に選ぶ
そして今回選んだOSSがこちらです。
Youtubeのクローンになっています。
3. インストールする
以下のようにインストールしてXcodeで開きます。
git clone git@github.com:aslanyanhaik/youtube-iOS.git
そして依存関係をインストールします。
4. 勉強方法のすすめ
以下のような方法で挑戦します。
- UI構築がどのようになっているのかを確認する
- 使用されているデザインパターンをみて見る
- swiftの言語の書き方で学べることがあれば学んでみる
- などなど
5. まずビルドする
まず、ビルドしてみると以下のような画面が見えます。
画像が大きいですが、まあ基本的なものは見れるのでよいとしましょう。
iPhone 8 plus から iPhone 8にする直りました。
6. 勉強する
UIレイアウト
例えば先ほどのホーム画面は以下のようになっています。
そしてStoryBoardでは以下の通りです。
ここからわかるのは、
- 赤い部分は
NavigationView
でできていて - 他は
TabelView
で定義している
ということです。
ぱっとみ、これくらいは想定通りです。
他にものぞいてみましょう。
アカウント設定画面は以下のようになっています。
ストーリーボードでは以下のようになっています。
また、お気に入りチャンネルは以下の通りです。
実際の具体的なコードはあとで見ていきます。
設計
以下のようなディレクトリわけになっていました。
YouTube ├── AppDelegate.swift ├── Assets.xcassets ├── Base.lproj ├── Extensions ├── Info.plist ├── LauchVIew.xib ├── Model ├── Supporting\ views └── ViewControllers
今度から真似てみようと思います。
コードから学ぶ
本記事では以下のHomeViewController
について学ぼうと思います。
また、一つでも画面を押すと以下のようになっていて、動画が再生されるようになっています。
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の方法は以下の本に詳しく載っていました。
- 作者: 成田元輝,杉浦颯太,小和瀬塁,山中大輔,末田正樹,藤野真聡,竹馬光太郎,桑原仁雄,倉長拓海,牧大輔,秋山卓巳,前田雅央,星北斗,末永恭正,久保田祐史,池田拓司,はまちや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)
ここでは以下のような構成なので、このようになっています。
あとのプロパティはそこまで重要ではありません。
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
詳しいことは以下の記事でご覧ください。
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
を知らなかったのですが、以下の記事で理解しました。
引数としてクロージャを受け取る時に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() }
次回
次回、このシリーズのエントリーを書くときは
アカウント周りをやろうと思います。