大阪のお好み焼屋です。今年も店に新メニューは増えませんでしたが、App StoreではWatch appとiMessage Stickers packをリリースできました! でもステッカーに関しては「そもそもiMessageを使っていない」というiPhoneユーザーが多くて、「フェデリギさん、話がちがうじゃないですか!」とけっこう驚きました。
さて以前RubyMotion+AutoLayooutでパスコードロック画面風ナンバーパッドをつくりました。当時はダイナミック言語+Terminal+Vimな環境で、UIからなにから全てスニペットもコードコンプリーションも使わずごりごり書いていてそれはそれで楽しかったのですが、あれから3年、いまはすっかりスタティック言語+Xcode+Storyboardな生活です。住めば都です。
最終目的
みなさんご存知パスコードロック画面です。完コピ、というわけにはいきませんがこれを目指します。
UIのトレース
Sketch
より再現度を高めるため画面の測定をするならドローイングソフトのSketchが便利です。Sketchはすごく使いやすいのでここ数年機会があるごとにつかっています(AppやステッカーのグラフィックはすべてSketch)。
しかし、残念ながらというべきか、僕が購入したときとくらべると、App Storeから抜けたり、値上げしたり、ついには年課金アップデートシステムに移行したりと、正直導入の敷居が上がってしまった感じです。でもいいソフトであることは間違いないと思います。
ねぎがべらぼうに高騰して苦しい時も、Sketchがあれば簡単にねぎを描くことができます。
おすすめしておきます。
UIトレースの手順
- iPhone実機でスクリーンショットを撮る
- 1をSketchにインポート
- [A]を押してその機種のサイズに合ったアートボードをつくる
- スクリーンショットをアートボードのサイズにリサイズ
- 測りたい箇所に線を引いたり描いたりしてそのサイズや距離を測る(Xcodeと同じようにoptionキーが使える)
ということになります。実例が以下。
明るい紫色がサイズ測定のためにSketchで描いたものです。
いろいろ線を引いていて気がついたことは、
- 画面の中央には(ナンバーパッドのセンターではなく)数字の5が来ていること(スクリーンショットの薄いグレイのレクタングルがナンバーパッドをセンタリングした場合の枠です)
- ステータスバーを除いて、ナンバーパッドの周囲には同じ程度の大きさの箱が5つ入る
- ナンバーパッドのリーディングから線を引くと、"Emergency"はすこしはみ出し、Cancelは逆にすこし内に入り過ぎのように見える
- 当たり前のようですが、ちゃんと計算されてデザインされているのだなあ、ということ。見習いたい。
必要な情報を集めたら、さっそく再現にとりかかりましょう!
Stack View とは
Stack ViewはAuto Layoutを補完するものとしてiOS9から加えられました。レイアウト上で共通の特性を持つエレメントをStack Viewへとその特性ごとに小さいものから大きいものへとスタックしてまとめていき、全体のレイアウトをよりシンプルに、簡単にしようという仕組みです。要するに「Divide and conquer」です。
最近たびたび耳にするLinkedInのLayoutKitもStackベースのようで、流行りのソリューションなのかもしれません。
従来の方法でナンバーパッドのような沢山のView(とそのサブクラス)が必要なUIをレイアウトしようとすると、なにか新しい部品をくわえるたびにいちいちXcodeからやれどこに置くのかわからないだの(ambiguity)、前に言ったことと相反しているだの(conflicts)ブーブー文句を言われたものですが、Stack Viewだととにかく一度スタックしてしまえば文句をいわれる的(マト)は絞れるようになる(Stack Viewをスケープゴートにできる)ので、だいぶ気が楽になります。
下が今回パスコード入力画面の再現のために使ったStack Viewを図示したものです。
画面内のすべての要素をスタックしました。NSMatrixと格闘した世代が見たら卒倒するんじゃないかというぐらいViewてんこ盛り状態ですが、UIStackViewはUIViewのサブクラスでありながらrenderの必要がないために特別な最適化が働いていて、動作は軽いんだそうです(参考)。NSCellもdeprecatedらしいので、これも時代の流れと割り切っていきましょう。
プロパティ
プロパティに関しては図解付きで丁寧にまとめられたものがあるので、詳しくはそちらをご参照ください。日本語ならこちら。
ここでは大雑把な話だけ。
Axis
従来のAuto Layoutの作法とくらべてStack Viewがもっともちがう点はどこかというとそれはこのAxisの存在だと思います。
もちろんいままでもhorizontal
とvertical
は重要な概念だったわけですが、Stack Viewでは「このStack Viewはhorizontalです」と決めると、とにかくそこに加えられた要素はすべて横並びにされます。そのStack View内での微妙な位置決めにはalignment
やdistribution
が使われるものの、横並びであることは変わりません。
var axis: UILayoutConstraintAxis
/*
enum UILayoutConstraintAxis: Int {
case horizontal = 0
case vertical
}
*/
ナンバーパッドではボタンが11個、左最下部を除いて綺麗に3x4で並んでいます。そこで今回は、まずはhorizontal
でボタンを3つスタックし、次にそのスタックを計4つに複製したら、最後にそれらすべてをまとめてvertical
でスタックすることにしました。
Storyboardでの実際の作業は以下のようになります。積み重なった(スタックされた)箱に下矢印がついたものがstack
ボタンです。
たくさんあった警告の赤いラインが、その場しのぎとはいえスタックで一瞬にして消えるのは見ていてとても気持ちがいい😊
あと、あらかじめオリジンとなるボタンのプロパティ(aspect ratioやclass)をカスタマイズしてから複製すると、後の作業がすごく楽になります。
Alignment, Distribution
AlignmentとDistributionはすごーくややこしいのですが、今回はボタンの大きさおよび列などすべて同じディメンションのため、verticalもhorizontalもそれぞれUIStackViewAlignment.fill
とUIStackViewDistribution.fillEqually
で大丈夫でした。
実現したいレイアウトがもっと複雑な場合は、先ほど紹介したサイトに図付きが解説がとても参考になると思います。
Spacing
上のgifを見ていただくとわかるように、最初にViewをスタックすると、まずはそれぞれのintrinsicContentSize
に基づいているであろうと思われるサイズに部品がリサイズされ、隙間もなくピタッとくっつけられます。
var spacing: CGFloat
spacing
はそれぞれarrangedSubviews
をaxis
の方向にその値分だけ間をあけてやる、というプロパティです。これらの値はSketchを利用して測量済みなので、そのまま適用してやります。この際、共通の値を共通のaxisを持つStack Viewに渡したければ、複数選択した状態にすると一括で入力ができます。
これだけで、すでに3年前に実現したことはほぼ達成できています! すばらしい。
Button
ボタンをレイアウト
せっかくなのでボタンの数字の下に英文字をプリントします。UIButton
に新たにレーベルを足したいんですが、StoryboardでUIButtonのインスタンスにlabelをsubviewとして加えようとしてもなぜかうまくいきません(原因はわかりませんでした。ハードコードすれば普通にできるはず)。
仕方がないのでUIViewをベースにして、以下のような構造にすることにします。
最背面から順に
- ベース(コンテナ)となるview
- 以下2つのlabelをverticalに束ねるstack view
- 大きいキーの番号用のlabel
- 小さい英文字用のlabel
- ボーダー(枠)だけ表示された最前面のbutton
- 以下2つのlabelをverticalに束ねるstack view
仕上げ
入力のインディケーターlabel4つをhorizontalにスタックし、"Enter Passcode"labelとverticalでスタックしたら、最後にナンバーパッドとまとめてverticalにスタックします。
それっぽいエフェクトをくわえたのがこちら。
まとめ
おまけとしてStack Viewで簡単に実現できるアニメーションも実装しました(参考)。秘密のパスコードを入力した方だけが見ることができます! ヒントは『屋号』ですが、じつはコードに直書き(!)だったりもします。
そもそもの話として Auto Layoutは難しい! というのはあると思います。でもその難しさを軽減する手段としてStack Viewは十分期待に応える出来になっていると思います。
ありがとうございました。