ライブラリでよくある動きをUIKitのみで

DIYしてみる(Part1)
Shinagawa.swift	#1

2016/11/25	Fumiya	Sakai
自己紹介と簡単な経歴など
✦ 今までの仕事履歴(本業)
石川県金沢市生まれ
本業はサーバーサイドのプログラマ	※Rails&PHP使い
26歳〜32歳: Webプログラマ(PHP	&	Rubyがキャリア長い)
23歳〜25歳: Webデザイナー兼ディレクター
チャンスがあればiOSアプリ開発も絶賛やってみたい!
趣味:シルバーアクセサリー集め・スイーツ作り・アプリ開発
女子向け・グルメ・エンタメ関連のお仕事が多い
Qiita	:	http://qiita.com/fumiyasac@github
Github	:	https://github.com/fumiyasac
✦ 酒井文也(さかい	ふみや)
東京(大塚)住まいの32歳
こんな格好を普段からしているので

遊び人に見られますがエンジニアです。
文系卒に思われますが

実は数学科で理系卒です。
めっちゃお酒好きそうに見えますが

ビール苦手でお酒も超弱いです。
今でもたまにUIまわりとか触りたく

なることがあったりなかったり
今年の4月からフリーランスです。

(割とお堅い感じの会社にいます)
最近のはまっている食べ物は

カボチャと担々麺と甘栗です。
最近はSwift以外ではRailsやLaravel・CakePHP・Node.jsなんかも
これまでに作ったもの(ネイティブアプリ)
①	簡易家計簿アプリ「Coffre」
②	ゲームアプリ「10秒虫食い算」
・カレンダーを自作しています
・シンプルなお小遣い帳感覚で支出管理できます
・全問正解者ほとんどいません…
・不定期ですがコラムも書いています
・サーバーサイドはRuby	on	Railsを使用
http://www.coffre.me/
・デザインにもこだわってみました(特にグラフ)
・実はちょっとバグがあります。
・問題は今後追加予定(現在110問収録)
個人的にはなりますが、他にもアプリ・Webサービスなど開発中です(2016年も宜しくお願いします)
・サイト等は次回のアップデートで公開予定
http://blog.just1factory.net/services/284
・若干の中毒性を含みます
カレンダーが好きでライブラリを作りました
日本の祝祭日を計算で出してくれる
・カレンダーアプリ等での活用を想定
・シルバーウィーク・ゴールデンウィークも対応
・ハッピーマンデー法の施行も対応
・春分の日・秋分の日にも対応
・過去の祝祭日もおおむね考慮はしている
構想や基本実装は僕ですが、他に3名のContributorのお力添えがあり実運用できるレベルになりました!
職人の手作業で計算しております!
・HTTP(HTTPS)通信は不要
★CalculateCalendarLogic	ver0.0.2
【だが本業は今もサーバーサイドです】PHP(メインのframeworkはSymfony)	&	Ruby(Sinatraに近いもの?)
・Github:	https://github.com/fumiyasac/handMadeCalendarAdvance
・実装解説:	http://qiita.com/fumiyasac@github/items/33bfc07ad36dfffcdf8f
・Github:	https://github.com/fumiyasac/handMadeCalendarOfSwift
✦ メリットもある反面デメリットもあるので見極めが大切になる!
UI系ライブラリの動きをDIYするために
以外と知らなかったクラスのことやUIKitの機能についても知れる
DIYすることで「このライブラリはこういう実装をしているのかも?」というアタリをつけやすくなる
ライブラリの動きを真似して自分でハードコードをしてみるとUIKitをより深堀りできて面白かったです!笑
★メリットと感じた部分
・UIKitの使い方やタイミングがわかるようになる
・後の実装での機能追加や仕様変更に対応しやすい
★デメリットと感じた部分
一見シンプルな動きに見えても考慮が必要部分が多い場合も
・自前でつくるのでライブラリ導入に比べて時間がかかる
・細かなパラメータや動きの調整でコードが複雑になる可能性
【どちらのことも考慮した上で実装の際の選択材料とする】
・デザインとの兼ね合いの考慮した上での実装が必要
・UIの動きを実現するアイデアがつかみやすい
実装のコストや

動きの微調整は

こだわると大変
こんなことをして

動いているという

理屈がわかる
✦ ライブラリを用いずにUI作成をするための自分が感じた勘所
取り組んだ所感と参考になった書籍
UIKitに関する部分は使っていないと忘れてしまいがちな部分なのでたまに見返すと発見があったりします。
★UIパーツやライフサイクル等に対する理解
★参考になった書籍
・iOSアプリのUI作成で必要なプロパティ・メソッドのリファレンス
・ライフサイクルやそもそもの理屈の部分にも触れられている
Delegate	&	Protocol Lifecycle
【UIパーツに関する理解&上記の4つに関する知識をしっかりと】
【UIKitの基本から理解したり、逆引きをする上でも助かります】
Swift3でも適宜読み替えて、差異がある部分についてはサイト等を活用する
・プッシュ通知やURLスキーム等の部分の解説も充実している
※Swift2.0ぐらいの時の本なので適宜読み替えが必要です。
Design	Pattern User	Interaction
UIKit	&	Swiftプログラミング(2015年5月)
✦ 今回のサンプルでピックアップするのは下記の4つに関して
よくあるアプリのユーザー操作で変化する例
ライブラリの機能の一部を利用する	or	一部の機能をカスタムしたいケースの場合はあえて自作する選択も
★ドラッグで下に隠れているサブメニューの開閉
★ナビゲーションバーやナビゲーションバー直下の表示部分を隠す
★パララックス効果
★スクロールと連動したボタン
https://github.com/aryaxt/iOS-Slide-Menu(参考ライブラリ)	iOS-Slide-Menu
各メニューのViewControllerを設定して重ねて表示させる	or	ContainerViewの活用
https://github.com/andreamazz/AMScrollingNavbar(参考ライブラリ)	AMScrollingNavbar
スクロール量や方向を受け取ってその変化量に応じてアニメーションを行う
https://github.com/adad184/MMParallaxCell(参考ライブラリ)	MMParallaxCell
UITableViewCell内のUIImageViewのAutolayoutの制約をスクロールを検知したタイミングで変更
(参考ライブラリ)	RMPScrollingMenuBarController
ボタンのクリックやボタンと連動するContainerViewやUIPageViewControllerの位置や表示状態の変更
※今回はこのボタン部分のみを自作しました
✦ 細かな部分の装飾や表現方法もこだわりもできるだけ真似てみる
今回の参考にしたアプリ一覧
それぞれのUIの中で気になった&再現したい部分を拾い集めた上でDIYして8割くらいを再現してみることに。
★下記のアプリを参考にして動きを参考にしました。
Facebook Local AWA
✦ 詳細に関しましてはサンプルのデモを行います。
今回のサンプルと詳細解説はこちら
今回は動きがある程度わかりやすいように合いそうなデザインと一緒にサンプルコードを作成してみました。
【画面内でデモを行いますので画面に注目して下さい】
	今回のサンプルについて
✦ 詳細解説に関しても下記の文章にまとめていますので併せてどうぞ
今回のサンプルと詳細解説はこちら
土台部分のContainerViewの重なりの記事やスライドと併せて、UI作成のご参考にして頂ければ嬉しいです。
	詳細解説及びサンプルはこちら	
Github:https://github.com/fumiyasac/SimplySideMenuSample
【サンプルはSwift3.0で作成しています】
	今回のサンプルについて	
SwiftでStoryboardとContainerViewを活用して、動きに合わせた細部のアレンジを加えたサンプル例

(iOSで良くあるライブラリを参考にDIYしました)
http://qiita.com/fumiyasac@github/items/eb5b17ab90f5aa27b793
JFYI:	土台となるContainerViewを利用したContainerViewの配置方法はこちら
http://qiita.com/fumiyasac@github/items/3218c35de5e59f3bfafa
各メニューのViewControllerを設定して重ねて表示させる	or	ContainerViewの活用
スクロール量や方向を受け取ってその変化量に応じてアニメーションを行う
UITableViewCell内のUIImageViewのAutolayoutの制約をスクロールを検知したタイミングで変更
ボタンのクリックやボタンと連動するContainerViewやUIPageViewControllerの位置や表示状態の変更
上記に挙げたUI上で綺麗に動かす&表現する上での参考になればとても嬉しく思います。
✦ 開閉処理を行う際の今回の処理ポイント
1.	ドラッグで下に隠れているサブメニューの開閉①
★(閉じている)コンテンツを表示している状態からいかに開くようにさせるか?
開く処理と閉じる処理を今回は別々に実装をしています。(もっとスマートな方法があれば知りたいです!)
コンテンツを表示しているContainerViewの左端のPanGestureを検知して、変化量を加算する
★(開いている)開いている状態から閉じようとするドラッグを実現させるか?
一番おおもとのView(self.view)のタッチイベントを受け取り、その変化量をContainerViewに加算する
画面左隅のPanGestureを取得する
タッチイベントの取得処理は一番おおもとのViewController.swiftに設定する
UIScreenEdgePanGestureRecognizer
//左隅部分のGestureRecognizerを作成する(デリゲートの設定と検知位置を決める)
let edgeGesture = UIScreenEdgePanGestureRecognizer(target: self, action:
#selector(ViewController.edgeTapGesture(sender:)))
edgeGesture.delegate = self
edgeGesture.edges = .left
//初期状態では左隅部分のGestureRecognizerを有効にしておく
mainContentsContainer.addGestureRecognizer(edgeGesture)
✦ 各ContainerViewのタッチイベントの受け取り状態に注意を!
1.	ドラッグで下に隠れているサブメニューの開閉②
★閉じている状態から開いている状態へ変化する
ドラッグ開始時にそれぞれのContainerViewのタッチイベント(isUserInteractionEnabled)の制御をする。
・UIScreenEdgePanGestureRecognizer

 (コンテンツのContainerViewに付与する)
・おおもとのView

 (self.view)
・コンテンツ表示用のContainerView

 (mainContentsContainer)
・サイドメニュー表示用のContainerView

 (sideMenuContainer)
・初期状態のUserInteractionの受け取り可否

 (isUserInteractionEnabledプロパティ)
sideMenuContainerはfalse	/	mainContentsContainerはtrue
・PanGesture検知時のUserInteractionの受け取り可否
sideMenuContainerはtrue	/	mainContentsContainerはfalse
1.	ドラッグで下に隠れているサブメニューの開閉③
今回の実装は開いてドラッグさせる処理においてはUIScreenEdgePanGestureの検知が開始トリガーになる。
★ポイントその1:

Storyboardでの重なり方

上から…

・透明ボタン

・コンテンツのコンテナ

・サイドメニューのコンテナ

・おおもとのView
★ポイントその2:

最終的な開閉状態の判定

(コンテンツのコンテナの位置)

ドラッグ終了時のX座標が…

・0以上〜160未満

→	コンテンツを閉じる

・160以上〜240以下

→	コンテンツを開く
✦ 同様に各ContainerViewのタッチイベントの受け取り状態に注意を!
1.	ドラッグで下に隠れているサブメニューの開閉④
★開いている状態から閉じている状態へ変化する
ドラッグ開始時にそれぞれのContainerViewのタッチイベント(isUserInteractionEnabled)の制御をする。
・UIScreenEdgePanGestureRecognizer

 (この状態はUserInteractionが無効にして検知不可)
・おおもとのView

 (self.view)
・コンテンツ表示用のContainerView

 (mainContentsContainer)
・サイドメニュー表示用のContainerView

 (sideMenuContainer)
・開いた状態のUserInteractionの受け取り可否

 (isUserInteractionEnabledプロパティ)
sideMenuContainerはfalse	/	mainContentsContainerはtrue
・コンテンツを戻した時のUserInteractionの受け取り可否
sideMenuContainerはtrue	/	mainContentsContainerはfalse
タッチイベントでX座標を取得
1.	ドラッグで下に隠れているサブメニューの開閉⑤
サイドメニューが開いた状態では、コンテンツのContainerのタッチイベントをすり抜ける状態を利用する。
★ポイントその1:

タッチイベントを取る範囲

・透明ボタン

・コンテンツのコンテナ

に変化量を加算していく

ViewController.swiftの

touchesBegan,	touchesMoved

touchesEndedに処理を記載
★ポイントその2:

最終的な開閉状態の判定

(コンテンツのコンテナの位置)

ドラッグ終了時のX座標が…

・0以上〜160未満

→	コンテンツを閉じる

・160以上〜240以下

→	コンテンツを開く
✦ UIScrollViewの中にボタンを配置する際にちょっと注意が必要
大まかなパーツの配置はAutoLayoutで制約を張り、細部はライフサイクルのviewDidLayoutSubviewsに任せる。
※この配慮をしておかないとAutoLayoutの制約で行うアニメーションの度にボタンが配置される
★ボタンや動くラベルの配置処理はviewDidLayoutSubviews内で一度だけする
2.	カテゴリー選択ボタン部分のスクロール&

ボタンタップ時の位置補正①
・ボタン等のUIScrollView内に配置する処理に関しては「1回だけ実行する」ようにしておく。
AutoLayoutの制約を変更するアニメーションを行うと、その後に再度viewDidLayoutSubViewsが実行
✦ 位置補正の処理はUIButtonのタグの値(Int)を利用して取得する
UIButtonを配置する際にbutton.tagで順番の値を保持しておき、ボタンタップ時にその値を動きに活用する。
★引数のpage:	Intの部分にタグの値を入れてアニメーションや位置調整に利用する
2.	カテゴリー選択ボタン部分のスクロール&

ボタンタップ時の位置補正②
UIScrollView内に配置したボタンのタップ時のアクションだけで実現ができる。
//ボタンのスクロールビューをスライドさせる
fileprivate func moveFormNowButtonContentsScrollView(page: Int) {
�//Case1:ボタンを内包しているスクロールビューの位置変更をする
�if page > 0 && page < (ScrollButtonList.buttonList.count - 1) {
scrollButtonOffsetX = Int(multiButtonScrollView.frame.width) / 3 * (page - 1)
�//Case2:一番最初のpage番号のときの移動量
�} else if page == 0 {
����scrollButtonOffsetX = 0
�//Case3:一番最後のpage番号のときの移動量
�} else if page == (ScrollButtonList.buttonList.count - 1) {
����scrollButtonOffsetX = Int(multiButtonScrollView.frame.width) * (ScrollButtonList.buttonList.count / 3 - 1)
�}
UIView.animate(withDuration: 0.26, delay: 0, options: [], animations: {
����self.multiButtonScrollView.contentOffset = CGPoint(
x: self.scrollButtonOffsetX,
y: 0
� )}, completion: nil)
}
コンテンツの切り替え等と

併用して使われる事が多いです。
UIScrollViewDelegateと

併用すると色々工夫も可能です。
✦ スクロールの開始位置と終了位置の差分を取得して実現する
UIScrollViewDelegateに関しては動きを持たせるUIを作成する際には重要なポイントになるのでしっかりと。
★UITableViewはUIScrollViewDelegateを利用できるのでこれを活用する
3.	下向きスクロールでナビゲーションを隠す動き①
UIScrollViewDelegateでスクロールのタイミングで起こる位置の取得処理に関する記載を行う
//スクロール開始位置を取得
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
//スクロール開始位置を格納するメンバ変数を準備しておき、スクロール開始したY座標を入れる
scrollBeginingPoint = scrollView.contentOffset
}
//スクロールが検知された時に実行される処理
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//スクロール終了時のy座標を取得する
let currentPoint = scrollView.contentOffset
//下方向のスクロールを行った場合の処理
if scrollBeginingPoint.y < currentPoint.y {
//(省略)自作メニューを隠して、変化量が40以上であればナビゲーションバーも一緒に隠す
} else {
//(省略)ナビゲーションバーを表示して、変化量が40以上であれば自作メニューも一緒に表示
}
}
✦ スクロールの変化量に応じてUIパーツの動きを実装する
スクロールの向きと変化量に応じてNavigationBarと動くボタン部分を動かすタイミングをハンドリングする。
★下記のような形でパーツが並ぶ	(お互いのパーツはAutoLayoutで引っ付くように設定)
3.	下向きスクロールでナビゲーションを隠す動き②
StatusBar	(背景のUIViewをコードで配置する)
NavigationBar	(スクロール量に応じて変化)
動くボタン部分	(スクロール量に応じて変化)
//NavigationBarを隠すメソッド
navigationController?.setNavigationBarHidden(Bool型 true「隠す」/ false「表示」, animated: Bool型 アニメーション有無)
★例.	下方向にスクロールを行った場合の変化量に対する処理
//変更したAutoLayoutのConstant値を適用する
topMenuConstraint.constant = 動くボタン部分のheight
UIView.animate(withDuration: 0.26, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: { finished in
})
②	変化量が40以上の場合
①	変化量が0以上40未満の場合
✦ UITableViewCell内に画像を配置して上下の制約を動的に変更する
セルに配置した画像の上下制約をOutlet接続をして値を変化できるようにし、変化処理メソッドを準備する。
★セルいっぱいに画像を配置して上下の制約をOutletで接続する
4.	UITableViewに配置した画像にパララックス付与①
上の制約をOutlet接続:topConstraint
下の制約をOutlet接続:bottomConstraint
★UITableViewCell内のメソッドの役割
override func awakeFromNib() {
super.awakeFromNib()
//(省略)意図的にずらした値を視差効果の計算用の変数にそれぞれ格納する
}
②	背景画像のAutoLayoutのconstant値を変更するメソッドを準備する
①	初期化のタイミング時
UIImageView	(上下左右のConstraintを0)
//背景画像にかけられているAutoLayoutの制約を再計算して制約をかけ直す
func setBackgroundOffset(_ offset: CGFloat) {
let boundOffset = max(0, min(1, offset))
let pixelOffset = (1 - boundOffset) * 2 * imageParallaxFactor
topConstraint.constant = imgBackTopInitial - pixelOffset
bottomConstraint.constant = imgBackBottomInitial + pixelOffset
}
スクロールの検知時&表示されていないセルに対してUITableViewCell(PictureCell)のメソッドを実行する。
4.	UITableViewに配置した画像にパララックス付与②
//スクロールが検知された時に実行される処理
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//パララックスをするテーブルビューの場合
if scrollView == parallaxTableView {
//画面に表示されているセルの画像のオフセット値を変更する
for indexPath in parallaxTableView.indexPathsForVisibleRows! {
setCellImageOffset(parallaxTableView.cellForRow(at: indexPath) as! PictureCell, indexPath: indexPath)
}
//(以下省略)
}
}
//まだ表示されていないセルに対しても同様の効果をつける
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let imageCell = cell as! PictureCell
setCellImageOffset(imageCell, indexPath: indexPath)
}
//UITableViewCell内のオフセット値を再計算して視差効果をつける
fileprivate func setCellImageOffset(_ cell: PictureCell, indexPath: IndexPath) {
let cellFrame = parallaxTableView.rectForRow(at: indexPath)
let cellFrameInTable = parallaxTableView.convert(cellFrame, to: parallaxTableView.superview)
let cellOffset = cellFrameInTable.origin.y + cellFrameInTable.size.height
let tableHeight = parallaxTableView.bounds.size.height + cellFrameInTable.size.height
let cellOffsetFactor = cellOffset / tableHeight
cell.setBackgroundOffset(cellOffsetFactor)
}
セル側のメソッドを呼び出す
UITableViewDelegate
UIScrollViewDelegate
✦ この部分はどの部品を使って再現できるかを考えてみる
(参考)アプリUIをパーツに分解してみる
UIKitについてある程度理解しておく&どんなことができるかを知るだけでデザインのヒントになる事が多い
★アプリ「Creema」を例に挙げて仮説を立てるとこんな感じ?
ページのスワイプに連動して

コンテンツがスライド
Navigation

Controller

(Push	Segue)
Push	hamburger

button
ボタンを押すと

隠れていたメニューを表示
UINavigationController?
UICollectionView?
UIScrollView?
UIScrollView?

+

ContainerView?
UICollectionViewCell?
UITabBar?
詳細ページのコンテンツが

横にスライドして表示される
UITableView?
ContainerView

を2段重ねにする?
UITableViewCell?
✦ ライブラリなしでUIを作るアプローチの知見は結構役に立つ
今回のまとめ
ご清聴ありがとうございました!またこのような機会があった際には是非ともよろしくお願い致します!
★ライブラリに近しい動きもUIKitの特性を組み合わせることで実現できるケース
UIまわりのリファクタリングや再実装を行うタイミングで作成&工夫ができる
★ライブラリを使う際の判断材料にもなる
どんな理屈で動いているかを知ることで検討中のライブラリについてもしっかりと理解ができる
★パーツに分解して考えることで実装のアタリをつける
要素ごとに細かく分けて「どのパーツを組み合わせているか?」の仮説を立てる
★自分ルール
【良いアウトプットのために】
発表・登壇時はこの中のいずれか2つを

絶対に準備するルールを設けています!

ライブラリでよくある動きをUIKitのみでDIYしてみる(Part1)