vol.2 ジャンル選択画面を制作する – SwiftでiOS用のクイズアプリを作る!

iOS Simulator Screen Shot 2015.04.05 23.32.27

今回は本アプリ内TOP画面(HOME画面)になる「ジャンル選択画面」を作成していきたいと思います。

ポイント

genre

今回の大きなポイントとしては画面を4分割するように配置するジャンル選択ボタンの実現方法ですね。

デバイスのサイズに合わせてぴったり幅や高さが半分になり、各ボタン間に隙間をどうやって空けるかがポイントです。

さらに、今回程度の目的ではあまり必要ないとは思いますが、できればこういったことはひとつひとつボタンを作るのではなく、配列や辞書などに必要情報を格納してループで自動作成したほうがメンテナンス性も上がるのでその方法についても模索していきたいと思います。

現状ではループでボタンを作成しつつAutoLayoutの制約をつける方法がまったくわかっていないので不安ですが・・・今回も欲張りすぎでしょうか(笑)?

想定されるやり方

  1. ボタンを管理する辞書を作成して必要情報を格納する
  2. 辞書をループで出力しつつAutoLayoutの制約を設定する

こう書くとすごくシンプルに見えますが、やり方のわからぬ現状ではループで出力しつつAutoLayoutの制約を設定していくという点について不安を感じるのが本音です。

もし実現できれば項目数の増減でレイアウトの幅や高さが自動調節されるようなモノも作れそうですので頑張って取り組んでみたいと思います。

実際の手順

結論からいうと、中途半端な動的レイアウトといった感じで完成させたというか、強引に終了させたというのが本音です。

まずは全コードを掲載した後にハマったところについて解説していきたいと思います。


import UIKit

class GenreViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    
    var myCollectionView : UICollectionView!
    
    let genres = [
        ["title" : "日本史\n世界史" , "tag" : "history"],
        ["title" : "アニメ\n漫画\nゲーム" , "tag" : "anime"],
        ["title" : "映画\n音楽" , "tag" : "movie"],
        ["title" : "ノンジャンル" , "tag" : "none"],
    ]
    
    //ステータスバーを非表示
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var screenSize = UIScreen.mainScreen().bounds
        var screenWidth = screenSize.width
        var screenHeight = screenSize.height
        
        let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
        
        layout.itemSize = CGSize(width: screenWidth / 2 - 0.5 , height: screenHeight / 2 )
        layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0);
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 1
        
        // CollectionViewを生成.
        myCollectionView = UICollectionView(frame: self.view.frame, collectionViewLayout: layout)
        
        //CollectionViewの背景を着色
        myCollectionView.backgroundColor = UIColor.whiteColor()
        
        // Cellに使われるクラスを登録.
        myCollectionView.registerClass(CustomUICollectionViewCell.self, forCellWithReuseIdentifier: "MyCell")
        
        myCollectionView.delegate = self
        myCollectionView.dataSource = self
        
        self.view.addSubview(myCollectionView)
    }
    
    //Cellが選択された際に呼び出される
    func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
        
        println(genres[indexPath.row]["tag"]!)
    }
    
    //Cellの総数を返す
    func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return genres.count
    }
    
    //Cellの中身の表示
    func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell : CustomUICollectionViewCell = collectionView.dequeueReusableCellWithReuseIdentifier("MyCell", forIndexPath: indexPath) as CustomUICollectionViewCell
        
        cell.textLabel?.text = genres[indexPath.row]["title"]
        
        let lineHeight:CGFloat = 30.0
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.minimumLineHeight = lineHeight
        paragraphStyle.maximumLineHeight = lineHeight
        let attributedText = NSMutableAttributedString(string: genres[indexPath.row]["title"]!)
        attributedText.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSMakeRange(0, attributedText.length))
        cell.textLabel?.attributedText = attributedText
        cell.textLabel?.textAlignment = NSTextAlignment.Center
        
        return cell
    }
}

class CustomUICollectionViewCell : UICollectionViewCell{
    
    var textLabel : UILabel?
    
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        // UILabelを生成.
        textLabel = UILabel(frame: CGRectMake(0, 0, frame.width, frame.height))
        textLabel?.text = "nil"
        textLabel?.backgroundColor = UIColor(red:0.29,green:0.53,blue:0.91,alpha:1.0)
        textLabel?.textAlignment = NSTextAlignment.Center
        textLabel?.textColor = UIColor.whiteColor()
        textLabel?.numberOfLines = 0
        
        // Cellに追加.
        self.contentView.addSubview(textLabel!)
    }
}

iOS Simulator Screen Shot 2015.04.05 23.32.27

そしてこれが完成した画面です。

以下、今回の大きなポイントやハマりどころについて書いていきます。

レイアウトにはUiCollectionViewを採用

UiCollectoinViewはiPhoneアプリの「写真」のサムネイル一覧画面のような表示をするのに使われる機能です。

前回の応用で縦半分横半分サイズで分割してViewを作成していく方法も考えたのですが、今回でUiCollectionViewをやり、次回でUiTableViewをやるとレイアウトの大まかなモノは一通り使うことになるので、今回は学習目的ということもありあえてUiCollectionViewでのレイアウトにこだわってみました。

十字にマージンを取る方法でハマる

やはりこれが最大の難所でした。

layout.itemSize = CGSize(width: screenWidth / 2 - 0.5 , height: screenHeight / 2 )
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0);
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 1

layout.minimumLineSpacing で「行」でスペースを空けることはできるのですが、「列」でスペースを取る方法はいくら探しても無いようで見当りませんでした。

iOS Simulator Screen Shot 2015.04.05 23.57.35

上画像をご覧になっていただくとわかると思うのですが、UiCollectionViewでは一行に表示するアイテムを左と右にくっつけるようにして「列のスペースで自動調節」するようになっているんですね。

なので基本的に「行のスペース」という概念はあっても、「列のスペース」という概念がそもそも無い、というのが私の仮説なんですがどうでしょね(苦笑)?

ともかくその仮説にしたがい、「表示アイテムの横幅を調節して列幅に隙間ができるようにする」ことでレイアウトを実現しました。

かなり強引な手法になって今回の4ジャンルのみでの対応になりジャンルを定義した配列数の増減で動的にレイアウト対応できるようにできなかったのが心残りです・・・。

UiLabelの改行でハマる

これは理解した今ならなぜ悩むのかすら不思議なのですが、Web出身者の方は同様にハマる可能性があるので書いておきます。

let genres = [
    ["title" : "日本史\n世界史" , "tag" : "history"],
    ["title" : "アニメ\n漫画\nゲーム" , "tag" : "anime"],
    ["title" : "映画\n音楽" , "tag" : "movie"],
    ["title" : "ノンジャンル" , "tag" : "none"],
]

このジャンルを定義した配列(辞書)内のtitleを表示するとなぜか改行コード(\n)以降が表示されないという現象が発生しました。

なので改行コードが間違っている、もしくは違うなどの可能性を疑い改行コード周辺を調べてみるものの特に異常は発見できず。

ならば!ということでUiCollectionViewのセルの仕様によるものかと必死で探してみるもののやはり特に問題を発見することはできませんでした。

後は思いつくワードで必死に探し続けた結果、UiLabelで改行をするには、

labelName.numberOfLines = 0

のように書けば良いことが判明しました。

・・・これ、私がWeb出身者であることが災いしたケースで、Webでは基本的にページがどこまでも下にいけるので改行に気を使う必要は基本的に無いので「わざわざ改行する場合に何らかの行動を必要とする」という発想が持てずに難しく考えて改行コードを調べたり各要素の仕様調査に発展してしまったんですね。

実はココに至るまでも幾度となくこの「Web界隈の常識」との差異にかなり苦しめられているんです・・・。

Web界隈出身の方は、まず仕様云々よりも「自分の発想・先入観」を疑う必要があるということを念頭に調べると良いと思います。

行間の調整も・・・あれ?

というわけでめでたく改行はできるようになったものの、今度は行間が思うような幅でなかったので調整することに。

前回の反省を活かしてなんらかのメソッドが既に用意されていて簡単にできるんだろうな〜と思っていたら大間違いでした^^;

let lineHeight:CGFloat = 30.0
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = lineHeight
paragraphStyle.maximumLineHeight = lineHeight
let attributedText = NSMutableAttributedString(string: genres[indexPath.row]["title"]!)
attributedText.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSMakeRange(0, attributedText.length))
cell.textLabel?.attributedText = attributedText

これが行間を調整する当該箇所のコードなのですが、一度NSMutableAttributedStringに変換してからでないと行間調整を設定することはできないんですね。

前項は調べつくした末に解決が簡単すぎて驚き、今回は前項のような簡単なものを想定したら面倒だった・・・こんなことの連続ですね。

セルをタップした時のアクション

これも当初はどう設定したら良いものか悩んでいましたが、UiCollectionViewにはきちんとそういった機能も予め用意されています。

//Cellが選択された際に呼び出される
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    println(genres[indexPath.row]["tag"]!)
}

これをクリックすると、ジャンル情報を定義した配列内辞書の「tag」がコンソールに表示されるようになっています。

iOS Simulator Screen Shot 2015.04.05 22.39.36

難しいことはないと思うのですが念のために書いておくと、「indexPath.row」で現在出力されたセルの番号を取得できるので、その番号を元にジャンル情報を定義した配列の当該情報を取得しています。

もちろんこんなことをするためにセル数は配列数から動的に取得する形式にしてあります↓

//Cellの総数を返す
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return genres.count
}

この頃はまだ配列数に応じて画面内の項目数に合わせて幅を自動調節される柔軟なレイアウトを夢見ていたんですけどね・・・。

次回はここで取得した値(tag)を遷移先画面へ持って行って当該ジャンルの問題タイトル一覧を出力するということをするわけですが、今回はとりあえずはここまでにしておきます。


今回はハマりどころが多かったために「何にハマったのか忘れた」という事態まで発生してしまって書ききれていない部分があります。

もっと細かくメモしながらやっていければ良いのでしょうけど、ハマっている最中はなかなかそんな余裕も持てませんよね・・・。

各ハマりどころについてもっと追求したかったというのが本音ですが、登場して一年も経たぬSwiftのただでさえ少ないネットリソースに私程度の英語力で海外サイトを巡り続けてもラチがあかないというのも事実。

これも時が経つごとにリソースの充実をみるはずなので今はとりあえず動くものを作り完成させることに終始し、いつしか再度必要になったときに調べ直して洗練をみれば良い、という方針で今後は進めることにします。

プログラムの問題に対して「時間が解決してくれる」というのもなんだか不思議な気もしなくはありませんが(苦笑)。

私にとってiOS開発は良くも悪くもWebで培った先入観を裏切るモノばかりで辛いです・・・一度、Webで上手にやれていたイメージを捨て去って初心者の気持ちで取り組まないといけませんね。

次回はココで選択したジャンルを元に所属する問題の一覧を出力して選択する画面を作成していきます。