標準 UI 要素とカスタマイズ
標準の UI 要素としてどういうものが用意されており、どうカスタマイズできるのかについては、以下のサンプルコードがざっくりと参考になります。こういうのが非エンジニアでも分かるものが欲しい…。
UIKit Catalog (iOS): Creating and Customizing UIKit Controls
ここから大きく外れると、カスタムなコントロールの作成などが必要になってくるのですが、アプリケーションの価値は UI のデコレーションに宿るとは思っていないので、実装コストをどこに費やすか、標準 UI のちょっとしたカスタマイズで代替できないか、というところを最初によく練った方がいいと思うのですよね。
tintColor によるカスタマイズ
tintColor は UIKit が提供している UI の標準の色を変更するプロパティで、子ビューに伝搬する という特性があります。
アプリケーションのビュー階層のルートは AppDelegate で管理している window になるので、以下の1行でアプリケーション全体に適用することができます。
window?.tintColor = UIColor.blue
アプリケーションのキーカラーが決まっている場合などはこの方法に頼りたくなるのですが、思わぬところまで影響してしまったりで、実際あまり使う機会がない気がします。
Appearance Proxy によるカスタマイズ
個別の UI 要素のインスタンスではなく、そのクラスそのものに対して、まとめて外観に関わるメソッドを実行することができます。
UIButton.appearance().setBackgroundImage(image, for: .normal)
上のような処理を行うと、UIButton およびそのサブクラスを使っているすべての場所に影響が及びます。実際にはサブクラスを用意して、そこに対して実行する方が安全です。
実行時に動的に影響するため、@IBDesignable
を有効活用している場合などは相性がよくないです。
UIView.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).tintColor = UIColor.red
appearance(whenContainedInInstancesOf:)
で影響範囲を限定することができるので、上のような指定をすると UINavigationBar だけ tintColor を設定するということができます。
pt(ポイント)と px(ピクセル)
iOS 上では pt という論理的な単位でレイアウトを扱います。1pt はデバイスのスケールに応じて 1〜3px の値になります。
scale | pixel | device |
---|---|---|
@1x |
1px | 絶滅危惧種 |
@2x |
2px | 最近の iPhone や iPhone SE は @2x
|
@3x |
3px | iPhone Plus や iPhoneX は @3x
|
ボーダーなどは 1pt では太すぎるため、ピクセルで指定して引きたいなどのケースがあるかと思います。pt を px に変換するには単に scale で割れば良いので実現は簡単です。
layer.borderWidth = 1 / UIScreen.main.scale
たま〜に 0.5 の固定値を入れる実装ミスを見かける気がします。
タイポグラフィ
システムフォントの日本語フォントは指定より小さくなる
大前提として、iOS には「システムフォント」というものが存在します。これは様々な言語のフォントを組み合わせたものですが、それだけではなく、視認性のために字間などの細かな調整が入っています。
このことが引き起こすよくある問題として、iOS9 以降のシステムフォントでは、和文が pt 指定通りにならない ということが挙げられます。
ともに 17pt を指定していても、システムフォント側の和文が明らかに小さいのが分かるかと思います。このため、Sketch で作ったレイアウトをピクセルパーフェクトにしようとしても大抵はうまくいきません。
iOS のヒラギノ角ゴシック問題
カンプとフォントサイズが変わってしまうのを避けるため、明示的にヒラギノ指定するという手段をとるという方法を思いつくかもしれません。
しかしながら、iOS のヒラギノ角ゴシックフォントは日本語以外を表示するときに欠陥を抱えていて、たとえば単に UIButton の font に指定すると、
このようにあっさり Descent 部分が見切れてしまいます。ネットを漁るとこの手の問題に対処するべく、先人がいろいろ調査していた痕跡を見つけることができると思います。
フォントの欠陥をワークアラウンドで解消することは不可能ではないですが、やはりベストなのは iOS のシステムフォントの特性を理解した上でデザインすることかなと思います。
参考文献
テキストの配置
欧文はそこまで神経質になる必要がないのですが、和文で特に約物が絡んだりすると 5〜10px は余裕でずれるので、ピクセル一致を目指すのはかなりつらみがあります。
なので、「どこのピクセルにテキストを配置するか」という情報よりも、近接している要素と、そうではない要素でどう余白を差別化するのか?というトンマナが汲み取れる方が嬉しい気がします。
余談として、Illustrator 経由で作られたっぽいカンプをトレースするのが非常に難易度が高い印象があるのですが、仮想ボディじゃなくて、字面体で bounding box を計算してたりするんでしょうか…。
入力のためのテキスト要素
UITextField や UITextView は、内部にパディングが設定されている ので、テキストの配置に使う場合は注意が必要です。
UITextView であれば、textContainerInset で調整すること自体は可能です。
UITextField の場合は、サブクラスを作成して、textRect(forBounds:)
、editingRect(forBounds:)
、placeholderRect(forBounds:)
の3つのメソッドをオーバーライドする必要があります。
それぞれ、入力済みのテキスト、編集中のテキスト、プレースホルダの位置を調整するものですが、プレースホルダは内部の UILabel
で表示しており、縦方向にセンタリングされているので y 軸方向にパディングを設定したときの挙動が怪しいです。
システムフォントの数値は固定幅ではない
何らかのポイントやタイマーの数値などを表示する場合、固定幅の方が桁数が揃って見やすい場合がありますが、iOS9 以降のデフォルトのシステムフォントでは数字もプロポーショナルで表示されます。
この場合、monospacedDigitSystemFont(ofSize:weight:)
を用いるか、または数字が固定幅で表示されるフォントを使用することで対応できます。
余白とデバイスの話
layoutMargin
すべての View は余白の値を持っていて、これは directionalLayoutMargins (iOS11 にて UPDATE: 語弊のある表現でした、コメントを参照ください)のプロパティで表されます。layoutMargin
から変更されるらしい
デフォルトでは 8pt の余白となっており、Storyboard で constraint to margin にチェックを入れて作った制約は View の外郭ではなく、この余白と制約になるため、思った通りの位置にならない場合の原因というケースが多かったです。
Safe Area と余白
この余白は、Editor > Canvas > Show Layout Rectangle
で可視化することができます。下の画像で薄い青い線で表示されているのが、View の持つ layoutMargins です。
constraint to margin をチェックして、親 View の layoutMargins に対してレイアウトしていくと、このように余白の 8pt が反映されます。
このとき、iPhone X において subview が Safe Area を意識した表示になっている のが分かると思います。Safe Area 領域を活用した込み入ったレイアウトを実現したい場合に、layoutMargins の存在を頭の片隅に入れておくといいのかもしれません。
なお、この挙動は insetsLayoutMarginsFromSafeArea
で切り替えることができます。
iPhone Plus の余白
iOS8 で追加された preservesSuperviewLayoutMargins
プロパティというものがあります。true
になっている View では、親 View の layoutMargins の値が尊重されます。
ググっても「UITableViewCell のボーダーを画面端に寄せるためのテク」くらいの情報しか出てこないので、どういう意図のプロパティなのか見えないのですが、デバイスを並べて UITableViewCell
の左右の layoutMargins を見ると微妙な違いがあるので、そこから推察するに、ファブレット端末で余白を大きく取りたいために追加したのかなと思っています。
iPhone X の Landscape 時の Safe Area の対応も含めると、UITableViewCell の左右の余白は、デザイナーが計算して考えるのではなく、OS の layoutMargins を尊重するという方針にした方が楽なのではという気がします。
iOS11 にて layoutMarginsがdirectionalLayoutMarginsに変更されるのではなく、layoutMargins はそのままにdirectionalLayoutMarginsが追加されたというのが正しいはずです。
layoutMarginsでは
left
・right
だったのがdirectionalLayoutMarginsではleading
・trailing
となっています(Auto Layoutでお馴染みですが主に右から左へ書き進める言語も考慮できるようにするための「左右」表現です)。「UITableViewCell のボーダーを画面端に寄せるためのテク」というより「標準スタイルのセルと左右の余白を揃えたい場合」によく使うように感じます(同じことを指そうとしているのかもしれませんが)。
Auto Layoutの設計ベストプラクティスと、Viewの種類ごとのテクニック集 - Qiita
多くのiPhone端末だと標準Cellの左の余白は15ptですが、iPhone Plusだと20ptですし、今後のあらゆる端末対応・OSアップデートを考えると数字で取り扱うべきではなく、親のテーブルビューのlayoutMarginsとの相対値で設定するべきで、preservesSuperviewLayoutMarginsを使うとそれがしっかりと実現できます。
その他、layoutMarginsを使いこなしていると、いくつか下の階層に伝えてそのlayoutMarginsからの相対距離でレイアウトしたいことが出てくるのですが、そのためのものです。
@mono0926 さん
ご指摘ありがとうございます。
layoutMargins のリファレンスに
という記載があったので、iOS11 からは、RTL を考慮していない
layoutMargins
よりも、directionalLayoutMargins
を使うべきなのかな?と思って書いたのですが、layoutMargins
自体は deprecated でもないので「変更される」という表現は誤りですね。あまり layoutMargins を使いこなせていなかったところがあり、勉強になります。
はい、これからは基本的には
directionalLayoutMargins
を使うべきだとは思いますちなみに、コードでAuto Layout組む場合、
layoutMargins
の各数値を取り扱うより、layoutMarginsGuide
からの距離で指定するのが大半だと思います。(僕の今のプロジェクトでも
layoutMarginsGuide
だけ使っていてlayoutMargins
は未使用でした。)UILayoutGuideではiOS 9から
leadingAnchor
・trailingAnchor
があって、iOS 11でようやくdirectionalLayoutMargins
としてそれを数値で取り扱えるようになったのかなと解釈しています。