vol.3 問題選択画面を制作する – SwiftでiOS用のクイズアプリを作る!

iOS Simulator Screen Shot 2015.04.09 3.10.19

前回のジャンル選択画面に引き続き、今回はジャンル選択後の「問題選択画面」を作成していきます!

ポイント

select

今回の作業は前回よりもシンプルで「UiTableView」を使って「クイズ問題の一覧」を表示する画面を作っていきます。

今回はクイズ問題データをjsonで予めアプリ内に用意してそこから出力するようなイメージで作成します。

多少難易度が高そうな作業としては、UiTableViewのセル内に更にUiViewを作成し、そのまた中にUiLabelを作成して配置していくことでしょうか。

本来はこの画面ではデータベースに格納された「クイズ履歴」を表示したいのですが、まだ先の作業が終わらないとそこまでは作れないので、ソコについては後日改めて書くことにします。

想定される作業手順

  1. 画面上に配置されたジャンル選択画面へ「戻る」ボタンとエリアを作成して戻れるように
  2. クイズ問題のjsonデータを用意
  3. 前回の画面からパラメータを受け取ってジャンル判別
  4. 当該ジャンルの問題をjsonデータから取り出して辞書に格納
  5. UiTableViewに問題データを繰り返し処理で全部表示して一覧を作成
  6. 問題タイトルや各種情報を表示するためのUiLabelを作成してセル内に配置

こんな感じでしょうか。

画面上の「戻る」ボタンに関してはナビゲーションうんたらが〜というのは何となく知っているのですが、今のところはやり方のイメージができていないところです。

jsonデータの取り扱いや、そもそも事前にこういったデータを用意する場合にjsonで良いのかどうなのかも不明なのですが、とりあえずやっていけばわかってくるか、ということでWebでも馴染みのあるjsonでやっていきたいと思います。

実際の作業

結論からいうとかなり大変な作業になりました。

そもそもこれまで一度もやったことのないことの連続なことに加え、これまで型に寛容なWeb関連言語ばかりやってきた私にはSwiftの配列や辞書周りの操作でかなり手こずってしまいました・・・。

まずは全体のコードから。

class ListViewController: UIViewController, UINavigationBarDelegate,UITableViewDelegate, UITableViewDataSource {
    
    var items:[Int] = []
    var titles = [Int:String]()
    
    //ステータスバーを非表示に
    override func prefersStatusBarHidden() -> Bool {
        return true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        //値を受け取る
        var appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        var genre = appDelegate.genre
        
        // jsonファイル読み込み
        var path = NSBundle.mainBundle().pathForResource("quiz", ofType: "json")
        var jsondata = NSData(contentsOfFile: path!)
        
        //swify
        var json = JSON(data:jsondata!)
        
        for ( i:String , v:JSON) in json[genre!] {
            self.items.append(v["id"].int!)
            self.titles[v["id"].int!] = v["title"].string!   
        }
                
        let displayWidth: CGFloat = self.view.frame.width
        let displayHeight: CGFloat = self.view.frame.height
        
        // TableViewの生成する(navigation barの高さ分ずらして表示).
        let myTableView: UITableView = UITableView(frame: CGRect(x: 0, y: 44, width: displayWidth, height: displayHeight - 44))
        
        myTableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "MyCell")
        
        myTableView.dataSource = self
        myTableView.delegate = self
        
        self.view.addSubview(myTableView)
    }
    
    override func viewDidAppear(animated: Bool) {
        
        let navigationBar = UINavigationBar(frame: CGRectMake(0, 0, self.view.frame.size.width, 44))
        navigationBar.backgroundColor = UIColor.whiteColor()
        navigationBar.delegate = self;
        
        let navigationItem = UINavigationItem()

        let leftButton = UIBarButtonItem(title: "戻る", style: UIBarButtonItemStyle.Plain, target: self, action: "back")
        navigationItem.leftBarButtonItem = leftButton        
        navigationBar.items = [navigationItem]

        self.view.addSubview(navigationBar)
    }

    // ジャンル選択画面に戻る
    func back(){
        let genreViewController: UIViewController = GenreViewController()
        genreViewController.modalTransitionStyle = UIModalTransitionStyle.CrossDissolve
        self.presentViewController(genreViewController, animated: true, completion: nil)
    }
    
    //Cellが選択された際に呼び出される.
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        var id = items[indexPath.row]
        println("押された問題のidは: \(id)")
    }
    
    //Cellの総数(jsonから取得した当該ジャンルの問題分だけ)
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return titles.count
    }
    
    //Cellに値を設定する.
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "MyCell")
        
        var id = items[indexPath.row]
        
        //問題のタイトル
        cell.textLabel!.text = titles[id]
        cell.textLabel?.font = UIFont.systemFontOfSize(14)
        
        //矢印アイコン
        cell.accessoryType = UITableViewCellAccessoryType.DisclosureIndicator
        
        //当該問題に関する情報を表示
        cell.detailTextLabel?.textColor = UIColor.grayColor()
        cell.detailTextLabel?.text = "挑戦回数:3回 正答率:80% 前回:2015.04.08"
        cell.detailTextLabel?.font = UIFont.systemFontOfSize(10)
        
        return cell
    }
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        
        //別画面へ遷移する前に呼ばれる処理
        println("移動します")
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

このコードで実現した画面が以下です。

iOS Simulator Screen Shot 2015.04.09 3.10.19

とにかくカタチにすることを再優先したのでオプショナルのnilチェックが甘い部分に関しては勘弁してください。

ナビゲーションバーの実現

これが今回はかなり特殊な事情があり、ナビゲーションバーが表示されるのがこの画面だけ、なんですよね。

    override func viewDidAppear(animated: Bool) {
        
        let navigationBar = UINavigationBar(frame: CGRectMake(0, 0, self.view.frame.size.width, 44))
        navigationBar.backgroundColor = UIColor.whiteColor()
        navigationBar.delegate = self;
        
        let navigationItem = UINavigationItem()

        let leftButton = UIBarButtonItem(title: "戻る", style: UIBarButtonItemStyle.Plain, target: self, action: "back")
        navigationItem.leftBarButtonItem = leftButton        
        navigationBar.items = [navigationItem]

        self.view.addSubview(navigationBar)
    }

この特殊事情にやられたのが「戻る」ボタンのアイコン「<」の表示です。 一般的なアプリ全体にナビゲーションバーを実装する場合はこういったアイコンは自動表示されるのですが、今回のように当該ViewController内でナビゲーションバーを定義して表示する場合にこの矢印アイコンが表示できなくて・・・。 現時点での結論としては、このやり方でシステムアイコンの矢印を表示することはできず実現するには自分で画像などで用意するしかない、という感じです。 なので今回はココについては妥協することにしました。実は半日ぐらいココでハマっていたんですけどね。こういうできそうでできないモノほど諦めがつかないものですよね。

画面遷移の値渡しについて

これについてはデリゲートを使うなどいくつかのやり方があるようですが、WebでいうところのGETのようなやり方だと以降の画面遷移時に値が保持されないのが気にいらなかったので、今回私が採用したやり方はPHPでいうところの「セッション」のようなやり方です。

    var genre: String?

まずはAppDelegate.swiftに保持したい値のための変数を定義しておきます。

        var appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        appDelegate.genre = &quot;ここに保持したい値&quot;

値を保存した箇所に上記を書きます。

        var appDelegate:AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
        var genre = appDelegate.genre

値を取り出したいところに上記を書けば、アプリ内のどこからでも保持した値にアクセスすることができます。

今回はクイズのジャンル情報を常に保持しておくのに使用しています。

クイズデータ(json)の取り扱いについて

結論からいうとクイズデータはjson形式のデータでアプリ内に持ち込むことにしました。

{
    &quot;history&quot;:
        [
         {
            &quot;id&quot; : 1 ,
            &quot;title&quot; : &quot;ここに歴史タイトル1が入ります&quot; ,
            &quot;quiz&quot; : [
                      {&quot;q&quot; : &quot;ここに問い1の内容が入ります&quot;,&quot;a&quot; : [&quot;答え1です&quot;,&quot;答え2です&quot;,&quot;答え3です&quot;,&quot;答え4です&quot;]},
                      {&quot;q&quot; : &quot;ここに問い2の内容が入ります&quot;,&quot;a&quot; : [&quot;答え1です&quot;,&quot;答え2です&quot;,&quot;答え3です&quot;,&quot;答え4です&quot;]},
                      {&quot;q&quot; : &quot;ここに問い3の内容が入ります&quot;,&quot;a&quot; : [&quot;答え1です&quot;,&quot;答え2です&quot;,&quot;答え3です&quot;,&quot;答え4です&quot;]},
                      {&quot;q&quot; : &quot;ここに問い4の内容が入ります&quot;,&quot;a&quot; : [&quot;答え1です&quot;,&quot;答え2です&quot;,&quot;答え3です&quot;,&quot;答え4です&quot;]},
                      {&quot;q&quot; : &quot;ここに問い5の内容が入ります&quot;,&quot;a&quot; : [&quot;答え1です&quot;,&quot;答え2です&quot;,&quot;答え3です&quot;,&quot;答え4です&quot;]}
            ]
         },
    //〜〜〜〜以下略〜〜〜〜
}

そして困ったのがSwiftのjson操作性の悪さ。

json内の下層データにアクセスするためにはとんでもないコードを書かなければならないという醜悪な仕様なんですね。

しかし、そういった人が不便に思うことに関しては解決策を考えてくださる型がいらっしゃるもので、今回はSwiftyJsonという素晴らしいライブラリを使わせていただきました。

これでjson操作がPHPなどと同じような感覚でやれるようになりました。

        // jsonファイル読み込み
        var path = NSBundle.mainBundle().pathForResource(&quot;quiz&quot;, ofType: &quot;json&quot;)
        var jsondata = NSData(contentsOfFile: path!)
        
        //swify
        var json = JSON(data:jsondata!)
        
        for ( i:String , v:JSON) in json[genre!] {
            self.items.append(v[&quot;id&quot;].int!)
            self.titles[v[&quot;id&quot;].int!] = v[&quot;title&quot;].string!   
        }

ここでローカルに保存した(バンドルした?)jsonデータを読み込み、SwiftyJsonで変換されたデータを配列と辞書に保存しています。

この辺りでSwiftの配列と辞書の洗礼を受けました。よくわからないエラーが出てとにかく色々やって解決、という根本的原因を判明させぬままに解決しました。

多分、配列と辞書の型とオプショナルについて不勉強なことが災いしたのだと思いますが、とりあえず今は前に進むことを再優先して調査は後回しにします。

UITableView周りについて

コレに関しては参考にしたサンプルが良かったこともあり特に迷うことはなかったです。

ただ、事前に想定していたセル内に「UILabelを作成して配置」は、そういったこともやはりできなくはないようですが今回はUITableViewが提供してくれている機能のみで画面を実現できたのでやりませんでした。

学習なのでやってみたほうが良かったのですが、他の箇所でハマりまくって時間をくってしまっていたこともあり心が折れました(苦笑)。


今回の画面はハマりどころが多くて時間がかかりました。

しかし、ハマっただけのことはあり「値の保持」や「データ(JSON)へのアクセス」のようなどんなアプリでも使うような技術についての知識を持てたのは良かったです。

ただ、今更なのですがこの形式でブログを書いていくと本作業の初期にハマったことなどを既に忘れてしまっていたりするんですよね・・・もし次回があるならもっと粒度感を小さくして常にメモしていくようなカタチで書くのが良いかもしれません。今回はもうココまできたので最後までこの形式でやっていきますが。

次からはこのアプリの核心である「クイズ画面」の作成に入っていきます。