はじめに
昨年の投稿の中で、iOSアプリでも特にメディア系のアプリよくあるようなUIを実現した簡易サンプルに関する説明をしました。(まだまだ拙い記事ながら100ストック以上していただき本当に感謝しています。)
ある程度複雑なUIを実現するためにはライブラリを使う方が開発工数や時間の短縮につながることが多いですが、ライブラリに対してカスタマイズを施したい場合や一部分の機能や動きだけを再現できていればよい場合には、UIを構成しうるクラスに対する理解や使い方をある程度知っておく必要があります。
今回は以前に作成したサンプルにAutoLayout対応をさせたものに加えてContainerViewを使ったサンプルとUIPageViewControllerを使ったサンプルを作成してみました。
私の開発環境
- OS:El Capitan 10.11.4
- XCode:XCode 7.3
- Swift:Swift2.2
もし間違いや指摘事項等がありましたら、github内でissueを立てたりコメント等いただければ幸いですm(_ _)m
1. UIPageViewController&ContainerViewに関してのおさらい
以前にも似た様なサンプル作成した際にContainerViewに関して取り上げたことはありますが、今回はサンプルが2つあるので、UIPageViewControllerに関してもざっくりと説明します。
★1-1. UIPageViewControllerはUIViewControllerをひとまとめにしてくれる便利なやつ
UIPageViewControllerの機能や使いどころをざっくりと解説すると下記のようになります。
■ 機能:
- 複数のViewControllerを子のViewControllerをまとめて管理してくれる
- スワイプで子のViewControllerを遷移できるようにする
- ページを左右へスクロールするアニメーション ※iOS6以降の機能
- 雑誌をめくるような動きをつけられる
- 指定したViewControllerへの移動
■ 使いどころ:
- SmartNewsをはじめその他キュレーションメディア系のアプリで良くあるUIを実現したいとき
- アプリの初回起動時に表示されているイントロダクションやチュートリアル画面をつくるとき
といったアプリのUIを考えていく上でも重要な部分になるかと思います。初心者向けの書籍ではContainerViewと同様に取り上げている物が少なかったのですが、下記の詳解記事やサンプルチュートリアルを参考にしました。
この後にも解説するContainerViewと比べてもViewController単位で管理ができるので
■ 基本を理解する上で参考になった資料アーカイブ:
■ 今回のサンプルを作成するにあたって参考になった資料アーカイブ:
- iOS Tutorial: Using UIPageViewController to create a content slider (Objective-C/Swift)
- [Swift] AndroidでいうViewPager的な左右にスワイプしてviewを切り替えるサンプルを書いてみた
- SwiftでUIPageViewControllerを使ってみよう
UIPageViewControllerを用いると「各コンテンツ用のViewControllerを子のViewControllerとして、親のViewControllerにて管理する」という使い方ができるので割とStoryboardも綺麗に保つことができます。また、今回のサンプルのようにタブメニューの動きに合わせてページめくりをする様なUIを作成する際には重宝すると思います。
★1-2. 改めてContainerViewに関しても少し補足説明をする
ContainerViewに関しては今回のサンプルでは「メイン部とメニュー部ViewController同士を重ねてひとつの画面に表示する」という部分やUIScrollViewの内部にViewControllerを表示させる部分で使用しています。
StoryBoardへContainerViewを配置しようとした際には"Enbed Segue"で一緒にViewControllerがくっついてくるので、各ContainerView毎に機能に応じてのViewControllerの分割ができます。ただ闇雲に多様してしまうとStoryboardが煩雑になってしまいがちなのでその点は注意したい点かと思います。
この特性を利用してContainerViewを重ねてボタンを押したら下に隠れているコンテンツを表示するといった表現も可能です。
■ 基本を理解する上で参考になった資料アーカイブ:
ContainerViewとUIPageViewControllerについてもまだまだ自分も完璧に理解しきれているわけでは決してないのですが、「あのアプリのUI実は結構気になっているんだよね」と感じた際の実装のアイデアやUI系のライブラリの勉強や活用する際に最初はざっくりとした理解でもあると、iOSのUI実装の際にはかなり役に立つのではないかと感じた次第です。
2. UIViewControllerのライフサイクルを利用したレイアウトの調整
AutoLayoutを用いたUIの設定はアプリ開発ではスタンダードになっていますが、慣れないうちはハマりどころとなってしまう場合も多いかと思います。(実際に私もそうでした)
私自身が実際にAutoLayoutを使っての実装をした際に感じたことや苦労した点や実装の上でのキモとなるかもしれないと感じている部分などを簡単にまとめてみました。
★2-1. AutoLayoutを使用した場合のアニメーション処理について
Storyboardで要素を配置してAutoLayoutで制約を設定した場合、アニメーションして見た目や大きさを変えたりする処理の書き方が最初わからなくって思わぬ苦戦を強いられてしまった部分でした。すでにAutoLayoutでStoryboard上に配置した要素に対してアニメーション等を利用して再配置やサイズ変更を行いたい場合には、
- AutoLayoutの制約の値を変更する(おそらくこっちの方が正しい手法ではないかと思われる)
- viewDidLayoutSubviewsメソッドをオーバーライドしてその中でCGRectMakeやCGSizeMakeを利用して配置要素の再定義する
のどちらかを使用する感じになります。今回のサンプルについては、自分がAutoLayoutをマスターし切れてないこともあってviewDidLayoutSubviews内に要素の再配置を使う形にしています。
※制約の変更を用いずにAutoLayoutを使用しているのに再配置するのは個人的に「本当にこれでいいんだろうか?」と感じますが、もしこのあたりの知見がある方がいらっしゃいましたらPullRequestや記事の修正等を頂ければ嬉しく思います。
★2-2. viewDidLayoutSubViews()の活用とUIViewControllerのライフサイクルについて
AutoLayoutを使用していない場合は、ViewDidLoad()内やViewWillAppear()内でStoryboardに配置したパーツに対してCGRectMakeやCGSizeMake等を使用して配置位置の決定ができます。
AutoLayoutを使用している場合は、上記のような指定の方法では動いてくれません(AutoLayoutは相対的な位置でレイアウトを決定するため)。viewWillLayoutSubViews()内やviewDidLayoutSubViews()にて位置の際決定を行うか、制約を変更して再度制約を設定し直す必要が出てきます。
今回の方針としてはAutolayoutを使用しているものについて(ContainerViewを使用したサンプル)は一旦大まかな要素に対してはで制約をつけてUIScrollViewの中にボタンを配置する箇所やUIPageViewControllerの位置決めに関してはviewDidLayoutSubViews()で再配置を行う実装にしています。
(この組み方で本当にいいのかは正直自信ないなぁ...)
この方法でする際にはUIViewControllerのライフサイクルについて知っていないとできない部分だったので下記の記事などを参考にしました。
■ UIViewControllerやUIViewのライフサイクルを理解する上で参考になった資料アーカイブ:
- UIViewのレイアウト周りの処理はviewDidLayoutSubViewsの内部でやった方がいい
- Auto Layoutでハマったframe,bounds値の取得:Swift/iOS8
- UIViewControllerのライフサイクル
- AppDelegate,UIViewController,UIViewのライフサイクル/iOS/Swift
- 画面めくりのUIを実装したいのならUIPageViewControllerがすごくいい (サンプルプロジェクト付き)
私自身もAutoLayouを始めたての頃はものすごく苦戦していましたし今でも、
- AutoLayoutとUIScrollViewの組み合わせ
- View要素のアニメーション
の2つは今でも苦手意識があるところなんで、これからも色々と試行錯誤をしてみようと思います。
3. サンプルの解説(UIPageViewControllerのサンプル)
UIPageViewControllerのサンプルに関してはこちらになっています。
最近のiOSアプリのUIの綺麗さに憧れてライブラリなしで、多少強引に作ってみたサンプルにはなりますが、自分なりにこちらのサンプルで実装した際のポイントになる部分や実装コードの解説をささやかながらまとめました。
★3-1. UIPageViewControllerとUIScrollViewを組み合わせてタブメニューでコンテンツを切り替える仕組みを実現する
今回のサンプルのUIPageViewControllerの活用方法に関しては親となるUIViewControllerの中に、
- タブ用のScrollView
- 各コンテンツ用のViewControllerを入れたPageViewController
の2つを動的に配置をして、PageViewControllerがページめくりをした場合ないしはScrollView内に配置されているボタンを押すとコンテンツが切り替わるようなUIを考えていきたいと思います。
■概念の理解と下準備:
このサンプルの一番のポイントとなる部分はそれぞれのコンテンツ用ViewControllerを下記の図のようなイメージでひとまとめにして管理するところになります。
- PageViewControllerを親として各ViewControllerを登録する
- 子の各ViewControllerの独立性は維持した状態になる
- PageViewControllerの機能を用いて切り替え時の動作を設定する
という特性を生かして実装を行っていく形になります。
まずは下準備としてStoryBoard上に親となるViewControllerとPageViewControllerに登録するコンテンツの中身になるViewControllerを設定しておきます(今回のサンプルでは6つ用意しています)
また、コンテンツ用のViewControllerを作成する際には右側のIdentifierの 「Storyboard ID」 の部分に任意の名前をつけてください。
■構造体「PageSettings」での定義内容について:
Storyboardの準備ができたら、いよいよ各パーツを配置していきます。
PageSettingsという構造体の中にScorllViewに設定するサイズや文言と共に、Storyboard上に設置したViewControllerに設定したIdentifierを元にViewControllerのリストを取得するメソッドを設定します。
//テーブルビューに関係する定数
struct PageSettings {
//ScrollViewのサイズに関するセッテイング
static let menuScrollViewY : Int = 20
static let menuScrollViewH : Int = 40
static let slidingLabelY : Int = 36
static let slidingLabelH : Int = 4
//ScrollViewに表示するボタン名称
static let pageScrollNavigationList: [String] = [
"🔖1番目",
"🔖2番目",
"🔖3番目",
"🔖4番目",
"🔖5番目",
"🔖6番目"
]
//UIPageViewControllerに配置するUIViewControllerクラスの名称
static let pageControllerIdentifierList : [String] = [
"FirstViewController",
"SecondViewController",
"ThirdViewController",
"FourthViewController",
"FifthViewController",
"SixthViewController"
]
//UIPageViewControllerに追加するViewControllerのリストを生成する
static func generateViewControllerList() -> [UIViewController] {
var viewControllers : [UIViewController] = []
self.pageControllerIdentifierList.forEach { viewControllerName in
//ViewControllerのIdentifierからViewControllerを作る
let viewController = UIStoryboard(name: "Main", bundle: nil) .
instantiateViewControllerWithIdentifier("\(viewControllerName)")
viewControllers.append(viewController)
}
return viewControllers
}
}
今回はUIPageViewControllerのインスタンスをプログラムで生成する関係上、それぞれのViewControllerをIndentifierから取得できるようにgenerateViewControllerList()
メソッドを作成しました。これをUIPageViewControllerのインスタンスが作られた後に、ViewControllerのリストを追加するという運びになります。
■ViewDidLoad内での定義内容について:
次にViewDidLoad内でポイントとなる部分をピックアップしていこうと思います。
この部分で行っている大まかな内容としましては、タブメニュー用のScrollViewの配置とViewControllerをまとめたPageViewControllerの配置になります。
ここではPageViewControllerに関する部分についてポイントを解説したいと思います。
手順1. UIpageViewControllerのインスタンスを作成
//UIPageViewControllerの設定
self.pageViewController = UIPageViewController(transitionStyle: .PageCurl, navigationOrientation: .Horizontal, options: nil)
//UIPageViewControllerのデリゲート
self.pageViewController.delegate = self
self.pageViewController.dataSource = self
まずはUIPageViewControllerのインスタンスを作成するところを見てみようと思います。
引数の中に、ページ送りをする際の方法や方向等を設定します。
- 第1引数:ページ送りする際のスタイル設定(.PageCurl:ページめくり / .Scroll:スクロール)
- 第2引数:ページ送りの方向設定(.Horizontal:水平方向 / .Vertical:垂直方向)
- 第3引数:その他オプション設定
上記の設定をするだけでPageViewControllerに登録したViewControllerを切り替える設定ができるので知っておくとUI設計や構築の幅が広がるのでとても便利です。
また、ページ送りをした際の処理を設定するためのデリゲートに関する記述も行っておきます。
手順2. 最初に表示するViewControllerを設定する
self.pageViewController.setViewControllers([PageSettings.generateViewControllerList().first!], direction: .Forward, animated: false, completion: nil)
この部分では読み込まれた際に初期表示をするViewControllerを決めます。
- 第1引数:表示対象のViewController
- 第2引数:アニメーションをする方向(.Forward:右から左 or 下から上 / .Reverse:.Forwardの逆)
- 第3引数:アニメーションの有無
- 第4引数:完了時の処理
今回は最初のViewControllerを表示するので、PageSettings.generateViewControllerList().first!
と設定すればOKです。
特に初期配置をする際はアニメーション要素はいらないのでanimated: false
としておきます。
手順3. UIPageViewControllerのインスタンスを大もとのViewに配置
ここはこれまでの手順で設定したpageViewController(UIPageViewControllerクラスのインスタンス)を配置すれば準備は完了です。
//UIPageViewControllerを子のViewControllerとして登録
self.addChildViewController(self.pageViewController)
//UIPageViewControllerを配置
self.view.addSubview(self.pageViewController.view)
ひとつだけ細かい点とすれば、おおもとのviewに追加するものがself.pageViewController.view
となる点くらいです。
■ViewDidLayoutSubViews内での定義内容について:
この中では、viewDidLoad内で配置した各要素に対しての位置の再定義とタブメニューの機能ScrollViewの中にボタンや動くラベルの配置・ScrollViewの設定をしています。
//レイアウト処理が完了した際の処理
override func viewDidLayoutSubviews() {
//UIScrollViewのサイズを変更する
self.menuScrollView.frame = CGRectMake(
CGFloat(0),
CGFloat(PageSettings.menuScrollViewY),
CGFloat(self.view.frame.width),
CGFloat(PageSettings.menuScrollViewH)
)
//UIPageViewControllerのサイズを変更する
//サイズの想定 →(X座標:0, Y座標:[UIScrollViewのY座標+高さ], 幅:[おおもとのViewの幅], 高さ:[おおもとのViewの高さ] - [UIScrollViewのY座標+高さ])
self.pageViewController.view.frame = CGRectMake(
CGFloat(0),
CGFloat(self.menuScrollView.frame.origin.y + self.menuScrollView.frame.height),
CGFloat(self.view.frame.width),
CGFloat(self.view.frame.height - (self.menuScrollView.frame.origin.y + self.menuScrollView.frame.height))
)
self.pageViewController.view.backgroundColor = UIColor.grayColor()
self.menuScrollView.backgroundColor = UIColor.lightGrayColor()
//UIScrollViewの初期設定
self.initContentsScrollViewSettings()
//UIScrollViewへのボタンの配置
for i in 0...(PageSettings.pageScrollNavigationList.count - 1){
self.addButtonToButtonScrollView(i)
}
//動くラベルの配置
self.menuScrollView.addSubview(self.slidingLabel)
self.menuScrollView.bringSubviewToFront(self.slidingLabel)
self.slidingLabel.frame = CGRectMake(
CGFloat(0),
CGFloat(PageSettings.slidingLabelY),
CGFloat(self.view.frame.width / 3),
CGFloat(PageSettings.slidingLabelH)
)
self.slidingLabel.backgroundColor = UIColor.darkGrayColor()
}
今回はPageViewControllerに関してはUIKitの部品を使用しませんでしたが、この部分については作りたいUIに合わせて適宜選んでいただければ良いかもと思います。
★3-2. UIPageViewControllerDataSourceを利用して選択されたコンテンツを表示する
必要なパーツ類の初期配置が完了したところで次は、ページめくりをした際の動きの部分を作成していきたいと思います。このサンプル内でPageViewControllerの中身がページめくりで変わるタイミングとしては、
- PageViewControllerの部分をスワイプしたタイミング → UIPageViewControllerDataSourceを利用
- ScrollView内のボタンをタップしたタイミング → ScrollView配置したボタン(Selectorに設定したメソッド)を利用
の2つになります。
■PageViewControllerの部分をスワイプしたタイミング:
PageViewControllerの部分をスワイプしたタイミングでコンテンツの移動処理を行う場合には、UIPageViewDataSourceのviewControllerBeforeViewController
またはviewControllerAfterViewController
を利用します。
Just FYI: UIPageViewDataSourceに関する公式ドキュメント
この中で行っている処理については、PageSettings.generateViewControllerList()
で取得したViewControllerのリストから現在のインデックスに+1
or -1
した値のインデックスのViewControllerを表示する処理を行っています。
//ページを次にめくった際に実行される処理
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let targetViewControllers : [UIViewController] = PageSettings.generateViewControllerList()
if self.viewControllerIndex == targetViewControllers.count - 1 {
return nil
} else {
self.viewControllerIndex = self.viewControllerIndex + 1
}
//スクロールビューとボタンを押されたボタンに応じて移動する
self.moveToCurrentButtonScrollView(self.viewControllerIndex)
self.moveToCurrentButtonLabel(self.viewControllerIndex)
return targetViewControllers[self.viewControllerIndex]
}
//ページを前にめくった際に実行される処理
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let targetViewControllers : [UIViewController] = PageSettings.generateViewControllerList()
if self.viewControllerIndex == 0 {
return nil
} else {
self.viewControllerIndex = self.viewControllerIndex - 1
}
//スクロールビューとボタンを押されたボタンに応じて移動する
self.moveToCurrentButtonScrollView(self.viewControllerIndex)
self.moveToCurrentButtonLabel(self.viewControllerIndex)
return targetViewControllers[self.viewControllerIndex]
}
また表示するViewControllerを切り替えた場合には、ScrollView内のボタンと動くラベルの位置の際は位置をする必要があるので、moveToCurrentButtonScrollView
とmoveToCurrentButtonLabel
で位置の再調整を行っています。
■ScrollView内のボタンをタップしたタイミング:
ScrollView内のボタンをタップした場合には、あらかじめ設定しておいたボタンのタグの値をもとにsetViewControllers
メソッドを利用してどのViewControllerを表示するかを決定します。
//ボタンをタップした際に行われる処理
func buttonTapped(button: UIButton){
//押されたボタンのタグを取得
let page: Int = button.tag
//UIPageViewControllerのから表示対象を決定する
if self.viewControllerIndex != page {
self.pageViewController.setViewControllers([PageSettings.generateViewControllerList()[page]], direction: .Forward, animated: true, completion: nil)
self.viewControllerIndex = page
//スクロールビューとボタンを押されたボタンに応じて移動する
self.moveToCurrentButtonScrollView(page)
self.moveToCurrentButtonLabel(page)
}
}
ScrollView内のボタンが押された場合には、PageViewControllerをViewDidLoadの時とは違いページめくりのアニメーションをつけたいので、第3引数をanimated: true
に設定しておきます。
★3-3. UIPageViewControllerを活用したライブラリ
今回の実装にあたっては、UIKitのPageViewControllerを使用しないで作成しましたが、コードでこの実装を行っている例がなかなか見当たりませんでしたが下記の記事での実装を参考にしました。
(OSS化もされているので今自分が作成しているアプリでも是非使ってみたいと思っています)
今回のサンプルで行った実装に関しては、タブメニュー部分がUIScrollViewでなおかつ数が決まっている形になっていますが、無限スクロールに対応するアレンジを加えたり、上記のリンクで公開されているようなライブラリを活用することで、UIの表現の幅がぐっと広まっていくのではないかと思っていますし、どんどん活用していこうと感じている次第です。
4. サンプルの解説(ContainerViewのサンプル)
ContainerViewを利用したサンプルに関してはこちらになっています。
こちらのサンプルについてはUI要素の重ね合わせを考慮したり、AutoLayoutとコードの配置が混在するので前述のサンプル以上にごちゃっとしているんですが、もし間違いのご指摘やよりスマートな書き方の提案等があれば幸いです。
★4-1. ContainerViewを用いて「重なっているサブメニュー」と「タブ付きScrollViewコンテンツ」を実現する
今回のサンプルではハンバーガーメニューを押すと隠れているメニューが現れる部分とUIScrollViewを利用したコンテンツ切り替えをContainerViewを用いて実現する形になります。
■概念の理解と下準備:
今回のサンプルを作成するにあたっての作業の流れはこんな感じになります。
- 土台のViewControllerにコンテンツのメイン部分とメニュー部分のContainerViewを配置してアニメーションや配置に関する処理を記述
- メイン部分のContainerViewに「Embed Segue」で紐付いているViewControllerにScrollViewを配置する
- ScrollViewの中に更にコンテンツ毎のContainerViewを配置する
- コンテンツ毎のContainerViewに「Embed Segue」で紐付いているViewControllerをコンテンツのControllerとして中身を記述
そして上記の手順一式をイメージ図にしてまとめると下のようになります。
今回のサンプルに関して、前述のUIPageViewControllerを利用したサンプル同様に重要な勘所となりそうな部分をピックアップして解説をしていきます。
■土台のViewControllerに関する解説「重なっているサブメニューを作る」:
ViewController.swiftの部分については、アプリの土台になる部分です。まずは土台となるViewControllerに下記の3つの要素を配置します。
各要素の重なりの順番は下から、
- (一番下)メニューとなるContainerView →
@IBOutlet var sideContainer: UIView!
- (真ん中)メインのコンテンツの表示を行うContainerView →
@IBOutlet var mainContainer: UIView!
- (一番上)メインのコンテンツをスライドさせるためのボタン →
@IBOutlet var hiddenButton: UIButton!
の3つの要素になります。
そして図のようにAutoLayoutの制約を下の図のように設定します。
そのあとはviewDidLoad内で各々の要素に対する初期設定、viewDidLayoutSubviews内で要素の位置を再定義に関する処理を記載していきます。
またアニメーションに関する部分はこのようにコードで記載しました。
//サイドのコンテナビューに関するenum
enum SideStatus {
case Opened
case Closed
}
//定数設定などその他
struct BaseSettings {
//ScrollViewのサイズに関するセッテイング
static let movedButtonX : Int = 268
static let closedButtonX : Int = 0
//ScrollViewのサイズに関するセッテイング
static let movedMainX : Int = 280
static let closedMainX : Int = 0
}
/**
* ↓↓↓↓↓ ※下記のメソッドはクラス内の記述になります ↓↓↓↓↓
*/
//ステータスに応じてメインコンテナの開閉を決定する
func judgeSideContainer(status: SideStatus) {
if status == SideStatus.Closed {
UIView.animateWithDuration(0.13, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
self.mainContainer.frame = CGRectMake(
CGFloat(BaseSettings.closedMainX),
CGFloat(self.mainContainer.frame.origin.y),
CGFloat(self.mainContainer.frame.width),
CGFloat(self.mainContainer.frame.height)
)
self.hiddenButton.frame = CGRectMake(
CGFloat(BaseSettings.closedButtonX),
CGFloat(self.mainContainer.frame.origin.y),
CGFloat(self.mainContainer.frame.width),
CGFloat(self.mainContainer.frame.height)
)
self.hiddenButton.alpha = 0
}, completion: { finished in
self.hiddenButton.enabled = false
})
} else {
UIView.animateWithDuration(0.13, delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
self.mainContainer.frame = CGRectMake(
CGFloat(BaseSettings.movedMainX),
CGFloat(self.mainContainer.frame.origin.y),
CGFloat(self.mainContainer.frame.width),
CGFloat(self.mainContainer.frame.height)
)
self.hiddenButton.frame = CGRectMake(
CGFloat(BaseSettings.movedButtonX),
CGFloat(self.mainContainer.frame.origin.y),
CGFloat(self.mainContainer.frame.width),
CGFloat(self.mainContainer.frame.height)
)
self.hiddenButton.alpha = 0.6
}, completion: { finished in
self.hiddenButton.enabled = true
})
}
}
上記のメソッドでは、
- ステータースのemumが.Openedのとき: → メインのContainerが右にずれる + 透明な閉じるボタンを活性
- ステータースのemumが.Closedのとき: → メインのContainerが元に戻る + 透明な閉じるボタンを非活性
という状態にします。
Just FYI:
ここまでで土台となるViewControllerに関する処理についての説明になります。
★4-2. UIScrollViewとAutoLayoutを使ってコンテンツの実体となるContainerViewを配置する
次にコンテンツ部分に関する設定をしていきます。
「メニュー部分のScrollView」と「コンテンツのScrollView」の表示部分を作成し、「コンテンツのScrollView」の中にコンテンツの実体となるContainerViewを配置します。
おおまかなレイアウトについてはAutoLayoutで制約を追加して、その他動的に配置したり細かな設定の部分はviewDidLayoutSubviewsでレイアウトの再定義を用いて調整を行います。
■土台のBaseContentsControllerに関する解説「タブ付きScrollViewコンテンツ」:
まずはBaseContentsController.swiftの設定から行っていきます。それぞれのパーツについては大きさとパーツの大きさや座標については下の図にある「初期配置」の数値に合わせて設定をして下さい。
またAutoLayoutで制約をつける順番としては、
- スペーサー用Label
- メニューScrollView
- メインScrollView
- 開くButton
- タイトルLabel
の順番でつけていってます。
このAutoLayoutの制約をつけ終わりましたら一度ビルドしてみてください。
正しく制約が設定できている場合はコンソールに警告が出ないと思います。
■コンテンツの数の分だけContainerViewを配置してScrollViewの中に格納してAutoLayoutで制約付け:
大枠の部分のレイアウトを設定したので、次はScrollView内に配置したContainerViewを配置していこうと思います。
まずはStoryBoardでBaseContentsControllerのSimulated Sizeを下の図のように変更します。
見た目的には横にグイっと広がります。今回はScrollViewの中に6つのContainerViewを配置していくので600×6=3600とWidthの値を設定します。
(下記に記すContainerViewのAutoLayoutの設定が終わった後にSimulated Sizeを元に戻すとStoryboard上でも元のサイズに戻った見た目になります)
次にScrollViewの中にContainerを6つ分配置していきます。ContainerViewを配置して(幅:600, 高さ:495)にして左から順にそれぞれ配置して下記の図のように制約をつけていきます。また、ContainerView自体の幅と高さを親のScrollViewに合わせたいので、ContainerViewから「Control+ドラッグ」をして親のScrollViewにカーソルを当てると、制約のポップアップが表示されるのでそこで、
- Equal Widths
- Equal Heights
の制約をそれぞれのContainerで追加してあげます。
ここまでし終わったら、AutoLayoutに関して出ている警告を「Update Frames」で解消してあげればScrollView内にContainerViewを均等に配置する作業が完了です。
ここまでの実装をまとめると下記のようになります。StoryBoardで見るとBaseContentsControllerの部分がグイっと広がっているのでいびつな感じに思えると思いますが、正しく制約が設定できた場合にはシミュレーターないしは実機でチェックしてみるといい感じになっているかと思います。
今回の実装を実装するにあたっては下記に記事で紹介されていた実装方法を参考にしてみました。
UIScrollViewとAutoLayoutの組み合わせに関しては、結構ハマりやすい部分ではあるので、今回の方法が果たしていい正解なのかはわかりません。ただ実際に手を動かして試行錯誤をする過程の中で徐々に慣れていくと良いかもと思います。
■UIScrollViewDelegateを利用して「コンテンツ部分のScrollView」と「メニュー部分のScrollView」の制御に関する部分を記述 & viewDidLayoutSubviewsでレイアウトの再定義:
viewDidLayoutSubviewsでレイアウトの再定義では、AutoLayoutで配置した部品(特にScrollView)に関する詳細な設定やScrollView内の表示領域等に関する設定を行っています。
メニュー部分のスクロールビュー内には、前のサンプルで紹介した際の方法と同様にコードにてで切り替え用のボタンと動くラベルを配置を行っています。
また、今回はScrollViewが2つあるので、コンテンツのScrollViewの時にページ送りをするようにするため、あらかじめタグ(tagプロパティ)を定義しておきその値を用いて判別をしています。
//スクロールが発生した際に行われる処理
func scrollViewDidScroll(scrollview: UIScrollView) {
//コンテンツのスクロールのみ検知
if scrollview.tag == ScrollViewTag.MainScroll.returnValue() {
//現在表示されているページ番号を判別する
let pageWidth: CGFloat = self.mainScrollView.frame.width
let fractionalPage: Double = Double(self.mainScrollView.contentOffset.x / pageWidth)
let page: NSInteger = lround(fractionalPage)
//ボタン配置用のスクロールビューもスライドさせる
self.moveFormNowButtonContentsScrollView(page)
self.moveToCurrentButtonLabel(page)
}
}
メニューのスクロールビューに関しても、その中に配置したボタンに応じて表示するスクロールビューの位置を決定したり動くラベルの位置を決定するようにしています。
※ moveFormNowButtonContentsScrollView
とmoveToCurrentButtonLabel
を参照してみて下さい。
■親のViewControllerに記載したメニュー開閉処理を子のContainerViewにつながったViewControllerより実行する:
右上に配置したボタンについては、BaseViewController.swiftと繋がっているContainerViewの位置をアニメーションで変更し、この下に隠れているメニューを表示させるためのものになります。
//ハンバーガーボタンを押下した際のアクション
@IBAction func viewControllerOpen(sender: AnyObject) {
let viewController = self.parentViewController as! ViewController
viewController.judgeSideContainer(SideStatus.Opened)
}
それぞれの親子関係を利用して、子から親のメソッドを実行することで実現させています。
今回は2種類の実装に関してピックアップをしてみましたが、「ViewControllerの重なりの部分はContainerViewを利用して、コンテンツの切り替えはPageViewControllerで行う」といったことも可能ですので、自分が作りたいアプリのUIに合わせて取捨選択をしたり、ライブラリの実装を追う際などのヒントに少しでもなれば幸いです。
5. UIPageViewControllerやContainerViewを扱うUIを実装してみて感じた事
iOSアプリ開発を始めたての時はAutoLayoutをキャンセルして、AutoResizingMask等を駆使してかなり強引な感じでレイアウトを作成していた感じのすすめ方でした。やっぱり最初は制約の概念等がなかなかピンとこなくって必要以上に時間を食ってしまうこともしばしばありました。
現在は始めたての頃よりも慣れてはきましたが、未だにやはり慣れなかったりpriorityなんかを調整する際は苦労することもあったりしますが、iOS9からは強力なUIStackView等も登場して少しずつ親しみやすくはなっています。
AutoLayoutに慣れていくためには「最初は簡単なサンプルからでいいので数をこなす」ことと「制約の概念をしっかりと理解する」ことに重点を置いていくと徐々に苦手意識が薄れていくのではないかなと思います。
またそれと同時にUIKitに関しても余力があるタイミングでAppleの公式ドキュメントやQiita等のTipsを参考に細かな部分やハマリがちな部分についても調べておくと実装の際にもヒントを見つけやすいのではないでしょうか。
あとがき
ここまでは「見よう見まねでライブラリに近しい動き」という個人的なテーマで色々まとめてみました。このサンプルで実務使用ではあくまで基本的な部分のみの実装にはなっているので、そのまま少し厳しい部分があるかもとは思います。
またこちらのサンプルに関しては多分に独学&我流混じりの知識での実装になっているので、
- もっとイケてる実装がこんな感じでできるよ
- いいライブラリを知ってるよ
などといった部分がきっとあるかとは思いますので、そんな際はコメントやPull Request等を頂きましたらとても嬉しいです。
今回の取り組みを生かして次回はライブラリを活用してより実際のアプリにより近しいような形での実装に関しても試してまとめてみようと思います。
追記とその他
2016.04.24
- 今回の記事のポイントとなる部分を「集まれSwift好き!Swift愛好会 #6」にて発表する機会がありましたので、その際に使用したスライドもここに共有致します。下記資料も皆様のご理解の参考となれば幸いです。
- 参考スライド: UIPageViewControllerとContainerViewでこんな見た目を実現するTips
- GithubへのPull Requestならびに要望や改善に関する提案も受け付けていますのでお気軽にどうぞ!