Swift de 脳トレアプリ 第2章

Blog Single

脳トレどころか、体すら動かさない自堕落な日々を送っていたためか、体のあちこちにガタが来ています←
そんな老体(ぇ)に鞭を打とうということで
今回はSwift de 脳トレアプリシリーズでございます!

相変わらず脈絡のない…←

第1章 目次

  • トップ画面を作っていこう (1)

トップ画面を作っていこう (1)

ちょっと日が空いてしまいましたが、前回はアプリの顔、タイトル画面を作成しました。
タップして次に来るのはトップ画面、メニューとかが並んでいる画面ですね。
今回からそちらを作成していきます。

以下は画面の完成イメージ図です。

まだ背景ちょっと保留な部分はありますが…
何度も言いますがデザインの酷さには目を瞑ってくださいませ。
自分自身が一番自覚しております←

…さて。
このトップ画面はタイトル画面に比べ情報量が少し多いですので、パーツごとに切り分けて作成していきます。
ざっくりとこのような感じでしょうか。

今回作って行くのはこの①と②の部分です。

STARTボタンの設定と配置

ViewControllerに処理を書いてボタンを配置します。
ここではイメージで作成したボタンの画像をそのままボタンとして機能させてみます。

// class HomeViewController

let startButton: UIButton = {
    let button = UIButton()
    let image = UIImage(named: "button_start")
    button.setImage(image, for: .normal)
    button.imageView?.contentMode = .scaleAspectFit // *1
    button.contentHorizontalAlignment = .fill // *1
    button.contentVerticalAlignment = .fill // *1
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}()

まずUIButtonのプロパティ定義。
ボタンに画像を設定するときはsetImage()メソッドを使用します。
ただこの時に気をつけておきたいのは画像のサイズ。
基本的には保存した画像のオリジナルサイズまでしか拡大されないので、
今回のように画像いっぱいをボタンとしたいときは、
ボタンサイズいっぱいに拡大縮小させる処理(*1の3行)を書いておくと確実です。

// class HomeViewController

override func viewDidLoad()
{
    // 省略

    startButton.addTarget(self, action: #selector(routeToTrainingList), for: .touchUpInside)
}

@objc func routeToTrainingList(_ sender: UIButton)
{
    // ボタン押下時の処理
}

次に、ボタンをタップした際のアクションを設定する必要があります。
これはviewDidLoad()メソッドに処理を書きます。
addTarget()メソッドでactionに押下時の処理メソッド名、forにどのイベントで発火させるかを指定するだけです。
うん、非常に簡単、分かりやすいですね。

あとは前回同様、引き続きviewDidLoadメソッド内でaddSubview()、AutoLayoutを設定…
…なのですが、せっかくなので前回より簡単に書けるAutoLayoutの設定方法をしてみましょう。

// class HomeViewController

override func viewDidLoad()
{
    // 省略

    startButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0).isActive = true // *1
    startButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 50.0).isActive = true // *2
    startButton.heightAnchor.constraint(equalToConstant: 126.0).isActive = true // *3
    startButton.widthAnchor.constraint(equalToConstant: 126.0).isActive = true // *4
}

iOS9以降で使用できるNSLayoutAnchorクラスを用いた方法です。
AutoLayoutの制約NSLayoutConstraintを生成するという目的は同じですが、
NSLayoutAnchorなら設定したいAnchorごとにconstraint設定でき、1行で有効化までできます。
例えば上記だと次のように設定されます↓

  • *1 viewのleadingAnchor(左端)から20.0ptのmarginを設定
  • *2 viewのbottomAnchor(下)から50.0ptのmarginを設定
  • *3 heightAnchor(高さ)を126.0ptに設定
  • *4 widthAnchor(幅)を126.0ptに設定

前回よりコードもかなり見やすくなりましたね!個人的にはこちらの方が分かりやすくて好きですね。
これでSTARTボタンの配置は完了です。

フッターメニューを作る

先ほどのSTARTボタンと同じようにボタンを配置していくわけですが、
全く同じことを3回しても面白く無いので、
アイコン部分のみを画像にして、他の要素はコードで設定してみましょう。

…と言っても、ここで一番悩むのがこのボタンの形。
そう、完成イメージではボタンがベベルされた形になっています。
もちろん、UIButtonに枠線をつけても真四角にしかなりません。
さぁどうしたものか。難しい形状を選ばなければよかっry←
そんな時はこれ!!!

UIBezierPath

今回のように自由な線を作る場合には、UIBezierPathが便利です。
簡単に言えば好きな座標に点を打っていくだけで線が描け、図形を作成できます。
ただdraw()メソッド内に書かなくては反映されないため、新しいクラスでメソッドをoverrideする必要があります。
作り方は次のように好きなx, y座標にaddLine()していくのがメインです。

// class BeveledButton

override func draw(_ rect: CGRect)
{
    let path = UIBezierPath()
    // 開始位置の設定
    path.move(to: CGPoint(x: bounds.minX + 15, y: bounds.minY))
    // 線を追加して行く
    path.addLine(to: CGPoint(x: bounds.maxX - 15, y: bounds.minY))
    path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY + 15))
    path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY - 15))
    path.addLine(to: CGPoint(x: bounds.maxX - 15, y: bounds.maxY))
    path.addLine(to: CGPoint(x: bounds.minX + 15, y: bounds.maxY))
    path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY - 15))
    path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY + 15))
    // パスを閉じる
    path.close()

    // 省略
}

=> BeveledButtonのコード全文はこちら

さらに色やサイズを指定するとこんな感じで表示されます。

ところで、フッターメニューのように、「複数の画面で同じ位置・同じ内容のパーツを配置する」ことはよくありますよね。
そのような繰り返される要素は、呼び出し・メンテナンスのしやすさを考慮して、独立のクラスを作成して処理を書くのがオススメです!
AngularのComponent概念と同じですね!
というわけでFooterMenuViewというクラスを作成して、書き進めて行きましょう。

先ほどのBeveledButtonを使ってフッターにボタンを配置しますが、
イメージを見てみると、このボタンは画像とテキストを縦に並べて表示する必要がありますね。
デフォルトでは横並びでボタンの中央に表示されてしまいますが、
EdgeInsets、簡単にいうとマージンを画像やテキストに入れることで縦並びにもできます。
基本としてはデフォルト状態の配置からポジションをずらしていくイメージです。
画像にはimageEdgeInsets、テキストにはtitleEdgeInsetsでそれぞれポジションを設定します。

let menuButton = UIButton()
menuButton.setImage(UIImage(named: "icon_brain"), for: .normal)
menuButton.setTitle("Training", for: .normal)

// テキストのマージン
menuButton.titleEdgeInsets = UIEdgeInsets(top: 0,
                                          left: -(menuButton.imageView?.bounds.size.width ?? 0),
                                          bottom: -(menuButton.imageView?.bounds.size.height ?? 0),
                                          right: 0)

// 画像のマージン
menuButton.imageEdgeInsets = UIEdgeInsets(top: -(menuButton.titleLabel?.bounds.size.height ?? 0),
                                          left: 0,
                                          bottom: 0,
                                          right: -(menuButton.titleLabel?.bounds.size.width ?? 0))

ここで気をつけておきたいことが2点あります。
1つ目はUIEdgeInsetsに使用するパラメータは定数定義しておかない方がいい、ということ。
上のコードのように画像のマージンにはtitleLabelのboundsサイズを指定していますが、imageEdgeInsetsを設定する前にtitleEdgeInsetsを設定しているのでサイズが微妙にずれます。
ずれたままの状態で設定するとレイアウトが崩れてしまいます。
リアルタイムのサイズを設定するのが望ましいですね!

2つ目は画像のサイズ。
ボタンに使用するものより大きいあるいは小さい画像を使用する場合、ボタンサイズが変わるとそれに合わせて拡大縮小されることがあります。
そうなると端末によって表示が違う、ということも。。。(なんとも厄介です
その場合は状況に応じて、冒頭でも使用したcontentModecontentVerticalAlignmentなどの設定が必要です。
表示がおかしい、と感じたらそちらで確認してみてください!←確認忘れが多い人が言っています

あとはマージンを微調整すれば、イメージ通りの縦並びに表示ができます。
ちなみにEdgeInsetsを指定しない場合と比較するとこのような感じです!


(左)横並び EdgeInsetsの指定なし
(右)縦並び EdgeInsetsの指定あり

ここまで

もう少しでフッターメニューのパーツは完成!
…ではあるのですが、この後の処理はじっくりご紹介したいので、次章へ続きます!

ここまでの動きはこのような感じです↓

おまけ

さきほどのここまでの動きを見て、ページ遷移時に黒い画面にローディングが表示されていたことにお気づきになった方はいますでしょうか?
実はよくあるローディング画面を入れ込んでました。

ローディング画面(LoaderView)で肝となるのがフェード
UIViewにあるanimate()メソッドで簡単に設定できます。
あらかじめViewの透過alphaを0で設定しておき、ローディングが実行された時にalphaをゆっくりと1に変更させる。
HTMLのCSSでもよく使う手法ですね。

// class LoaderView

func fadeIn()
{
    UIView.animate(withDuration: 0.5, delay: 0.0, options: .curveEaseIn, animations: {
        self.alpha = 1.0
    }, completion: nil)
}

=> LoaderViewのコード全文はこちら

かなり簡素なローディング画面にしてしまったので、デザインは時間があれば作り直そうかなと…
ローディングの種類によっておそらく他にも色々と手法があると思いますので、ご参考までに!

最後に

今回はボタンづくしでしたので、色々な書き方をご紹介しましたが、
レイアウトは端末によるズレなども気にしないといけないので、そこはなかなか難しいですね。
これまで何度手詰まりしたことか…
個人的にはUIBezierPathとanimate()でもっと複雑な処理を書いてみたいですね。

次回はUICollectionView特集です!(ぇ

次章予告

  • トップ画面を作っていこう (2)

参考

Swift de 脳トレアプリシリーズはGitHubに公開しています!

Posted by Mao Miyaji
千葉にある夢の国を愛して止まない、元「魚のお姉さん」のエンジニア。PHP, TypeScriptメインで、暇さえあれば色々な言語を一かじり。

Other Posts: