143
142

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

メディア系アプリでよくあるUIを実現した簡易サンプル

Last updated at Posted at 2015-12-08

一度は憧れたことがあるメディア系アプリのUI

最近のキュレーションアプリやファッション情報系のアプリでよくある、スワイプすると写真付きコンテンツリストの一覧が切り替わり、なおかつコンテンツの上部分に設置されたタブが切り替わるようなUIはよく見かけると思います。このようなアプリを作成するための足がかりとしてまずはUIScrollViewとContainerViewを用いて、アプリの骨格となる部分を作成するための方法とポイントをご紹介致します。

ContainerViewに関してざっくり説明&導入する際のポイント

以外と参考書や書籍などでも扱っているものが少ないContainerViewですが、特に1つのベースとなるViewControllerに複数のViewControllerを配置して複雑な画面構成をする際には重宝します。
InterfaceBuilderで「Container」という要素をStoryboardに向けてドラッグ&ドロップをしてみると、ContainerViewはViewControllerがあらかじめくっついた(Embed Segueで紐付いた)状態のパーツになっています。

いうなれば「入れ物」の名の通りViewControllerをまとめてくれる、使いこなせるようになるとなかなか便利なやつです。

※ Webのiframe(インラインフレーム)をイメージしていただくと良いかと思います。

メリットとデメリットを個人的な雑感と視点から

以下は私自身の個人的な使用感や感想も多少はありますが、主に以下のメリット・デメリットがあるように感じました。

★メリット
  • 複数の子供のViewControllerに処理を分割でき、ContainerViewにembed segueされているViewControllerは独立して振舞うことができます。
  • 機能が全く異なるViewControllerを共存させること

特にStoryBoardで遷移図が直感的にイメージがしやすいところが私は特に気に入っています。

★デメリット
  • ContainerViewを多用しすぎた場合、かえってStoryboardが煩雑になって見辛くなってしまう
  • 子のViewControllerから親のViewController(その逆も然り)へ値を渡す際の処理が複雑になってしまいデバッグに戸惑うケースもあった

ただしStoryBoardとの相性が良くって便利だからといって闇雲に使いまくると設計が破綻しやすくなるのでこれまた注意が必要かもですね。

サンプルソースの概要

★ 私の使用環境:

OS:Yosemite 10.10.5 ※まだEl Capitanにはしていません><
XCode:XCode 7.2
Swift:Swift2.1.1

★参考にしたアプリ:Ciel

サンプルではありますが、トーン&マナーがしっかりと整った印象があったので、ボタンやスライダーの挙動等の参考にしてみました。

★Github:mediaStyleContainer

今回は上記のサンプルをベースにポイントとなる部分を解説します。

★画面に関して

<タブビュー画面>

Simulator Screen Shot 2015.12.10 6.14.50.png

<進むボタンを押した時>

Simulator Screen Shot 2015.12.10 6.15.00.png
★注意点

本サンプルはAutoLayoutを用いていませんので、AutoLayoutを使用している場合には適宜カスタマイズして頂けますと幸いです。

処理に関しての説明

1. どのスクロールビューが操作されたかを識別する

このサンプルでは、

  • ボタンを横一列に配置したScrollView
  • コンテナビューを横一列に配置したScrollView

の2つが共存する形になります。

スクロールが検知された際に呼び出される下記のメソッド
scrollViewDidScroll(scrollview: UIScrollView)
に関しては、コンテナビューを表示するスクロールに対してだけ呼び出すようにしなければいけません。

そこで下記のようにscrollViewのtagプロパティを設定してタグの設定値によって上記の処理を実現します。

ViewController.swift
self.buttonScrollView.tag = ScrollViewDefinition.ButtonArea.returnValue()
self.contentsScrollView.tag = ScrollViewDefinition.ContentsArea.returnValue()

そしてコンテナビューを表示するスクロールに対しては下記の様します。

ViewController.swift
func scrollViewDidScroll(scrollview: UIScrollView) {
        
    //コンテンツのスクロールのみ検知
    if scrollview.tag == ScrollViewDefinition.ContentsArea.returnValue() {
        
        //現在表示されているページ番号を判別する
        let pageWidth: CGFloat = self.contentsScrollView.frame.size.width
        let fractionalPage: Double = Double(self.contentsScrollView.contentOffset.x / pageWidth)
        let page: NSInteger = lround(fractionalPage)
            
        //動くラベルをスライドさせる
        self.moveFormUnderlabel(page)
            
        //ボタン配置用のスクロールビューもスライドさせる
        self.moveFormNowButtonContentsScrollView(page)  
    }

}

2. 子のViewControllerから親のViewControllerの遷移処理を実行する

今回のサンプルは、子のViewControllerから親のViewControllerの遷移処理をしています。
遷移処理の書き方は下記の2パターンがあると思います。

子のViewControllerから親のControllerのSegueを実行
pattern1_container.png

子のViewControllerからpresentViewControllerメソッドを実行
pattern2_container.png

今回は、パターン1を採用して子のViewControllerで設定した値を遷移先の親のViewControllerで使用するようにしています。

ChildContainer1.swift
//子コンテナのボタンを押した際の処理
//ChildContainer1〜ChildContainer6も同様
@IBAction func sendParent1(sender: UIButton) {
        
    let resultDictionary: [String : String] = [
        "id" : "1",
        "color" : "CCFF66",
        "name" : ButtonTextDefinition.getButtonLabel(0)
    ]
    //親コンテナのセグエを実行する&値を渡す
    self.parentViewController?.performSegueWithIdentifier("fromChildController", sender: resultDictionary)
        
}

親のViewControllerではセグエ実行前の処理に子のViewControllerから渡ってきた値をセットしてあげます。

ViewController.swift
//segueを呼び出したときに呼ばれるメソッド
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

    if segue.identifier == "fromChildController"{
   
        /**
         * ■ このサンプルのポイント
         * 
         * 子供のコントローラーで遷移を行うボタンに下記の記述を施す 
         * (StoryBoardで任意のViewControllerにContainerを配置した場合、ContainerでEmbed Segueで繋がっているViewControllerは、配置されたViewControllerの子供のコントローラーとして認識される)
         * self.parentViewController?.performSegueWithIdentifier("fromChildController", sender: resultDictionary)
         *
         * → このメソッドで渡されたsenderの値を遷移先のControllerへ引き渡しをする
         */

        let dataList: AnyObject = sender as! [String : String]
        let resultController = segue.destinationViewController as! ResultController

        resultController.sendColor = dataList["color"] as! String
        resultController.sendLabel = dataList["name"] as! String
    }
}

その他

ContainerViewとScrollViewの合わせ技に関する部分については、私が作成した勉強会のスライドで恐縮ですが参考になれば幸いです。

Just FYI

本来ならばAutoLayoutにも対応したり、より詳細な設定ができるようにenumやstructを活用して、お使いになる方が迷わないような共通化やカスタマイズ・リファクタリングを行う必要があると思います。
昨今では上記と類似したようなふるまいを実現するためのライブラリやサンプル・アプリも公開されています。

他のページネーション系ライブラリのPickUp

※各Githubのページへ遷移します
PagingMenuController
PageMenu

Qiitaでも上記のライブラリの導入事例や実装例の過程などを掲載した記事もあるので、ぜひ探してみるとさらに参考になると思います。
実際の現場や実務では工数短縮や複数人開発等のことも考慮し、ライブラリを導入することが多いと思います。ですが今回は実際に、

  • どんな仕組みで動いているかを知りたい
  • CocoaPodsやCarthageなしで作ってみたい

という思いがありましたので、実装してみた次第です。

私自身iOSの開発を始めてまだ1年足らず(実務経験は3週間だけ)ぐらいのものですが少しでも参考になれば幸いです。

追記とその他

2015.12.10:更新

  • バージョンや使用環境の追記
  • サンプルの見た目が作りかけそのまんまでちょっとダサかったので少し手を入れました。
    (挙動は特に変えてはおりません)

2015.12.20:更新

2016.12.21:更新

  • こちらのサンプルコードをSwift3に対応しました。

このサンプルについて

  • GithubへのPull Requestならびに要望や改善に関する提案も受け付けていますのでお気軽にどうぞ!
143
142
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
143
142

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?