Kekeの日記

エンジニア、読書なんでも

AutoLayoutをを使ってフォームをコードでレイアウトする

0. はじめに

今回はiOSのログインのViewをAuto Layoutをコードベースで定義して、使って行こうと思います。

f:id:bobchan1915:20180830160521p:plain

参考UI

アイコンや背景色などコンテンツが必要な箇所は実装できないので、省略しています。

 1. Auto Layoutとは

Auto Layoutとは

Viewの相関関係に基づいて制約をすることによってレイアウトする手法です。

1.0 なんで大事なの

一言でまとめるとレスポンシブのためです。

iOSデバイスは昔と違って、いろんな大きなの種類があります。

それら一つ一つにレイアウトを決めても効率が悪いので、まとめてレイアウトしようというわけです。

1.1 制約とは

制約とは、Auto Layoutに用いるインターフェースオブジェクトの位置や大きさの関係性のことです。

自然言語でいうと「この長方形のオブジェクトを縦10px, 横12pxで中央揃えして」といったようなことが制約に当たります。

 1.2 制約式とは

制約式とは制約を実際のコードで定義したものです。

特にAuto Layoutでは、このような制約式の連立方程式を解いて、レイアウトを定めています。

具体的には

Botton1.top = Botton2.top + 8

のようなものです。他のインターフェースオブジェクト以外にも、親クラスsuperviewなども参照することができます。

1.3 制約の定義方法

制約を定義するには以下の二つの方法があります。

  • NSLayoutAnchorを使う方法
  • Intrinsic Content Sizeを使う方法

1.3.1 NSLayoutAnchor

NSLayoutLayoutとは、iOS9から追加された制約を制定するためのクラスで、開発者が明示的に制約オブジェクトを生成します。

詳しくは知りませんが、昔はNSLayoutConstrainだったみたいですが、利便性と安全性を備えたものに進化したみたいです。

公式ページは以下のようにのなっています。

Auto Layoutガイド: プログラムによる制約の生成

良記事は以下のものです。

qiita.com

1.3.2 Intrinsic Content Size

Intrinsic Content Sizeとは、あらかじめオブジェクトの持つコンテンツによって決定される制約の定義方法です。

たとえば二つのUITextFieldのテキストフィールドを作ったとして

ひとつは

  • a

でもう一つは

  • aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

とすると、二つ目の方が小さくなります。

1.4 xibファイル

xibファイルとはApple独自のxmlフォーマットでStoryBoardと同等の役割をします。

2. 実際に作る

2.1 配置は考えずにフォームを作成

まず、フォームを作成します。

せっかくAuto Layoutを使うので、コードベースで生成したいと思います。

まず、インターフェースオブジェクトを生成します。

let button = UIButton()

仮によくわからないオブジェクトはShow Quick helpでヘルプを見ることができます。 また、もっと詳細は一番下のClass Referenceで見ることができます。

f:id:bobchan1915:20180830161328p:plain

特にUIButtonには以下のようなプロパティを持つ。

  • 外接矩形: インターフェースオブジェクトから装飾を除いたもの
  • フレーム: 装飾までを含めた大きさ

ここでCGReck型で指定することになります。

ここで自分のviewの中央に配置したいので、self.view.bounds.width/2などと定義します。

ここでboundsframeの違いが大事なので以下の記事を参考にしてください。

stackoverflow.com]

簡単にまとめると

term 説明
frame 親ビューの座標系からみての座標
bounds 自身の座標からみての自分

つまり必ずbounds.origin.xは0であり、y座標も然りです。

また、デフォルトでは、背景もUIColor.white、文字色もUIColor.whiteなので、初期画面では何も見えません。

それはCGRectの仕様で、以下のように座標をとるからです。

f:id:bobchan1915:20180830173522p:plain

引用

ボタンを配置まで、できあがったコードが以下の通りです。

 class LoginViewController: UIViewController 
 let formWidth:CGFloat = 300
    let formHeigth:CGFloat = 40
    
    let button = UIButton()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        button.setTitle("SIGN IN", for: UIControlState.normal)
        button.backgroundColor = UIColor.black
        button.frame = CGRect(x: (self.view.bounds.width - formWidth) / 2, y: self.view.bounds.height / 2, width: formWidth, height: formHeigth)
        
        self.view.addSubview(button)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

ビルドすると以下のようになってます。ここまでは目的通りです。

f:id:bobchan1915:20180830180518p:plain

ここでUITextFieldを作っていきます。 UIButtonと全く一緒です。それぞれにプレースホズダーを設定しました。

f:id:bobchan1915:20180831114846p:plain

次にUIImageをロゴ用に配置したいと思います。 無料でロゴを作れるサービス「hatchful」を使いました。

hatchful.shopify.com

他のものと同様に制約を課してみます。

また、Assets Catalogに追加します。

UIImageは「画像がない」のケースがあるので初期化するとオプショナル型が返ってきます。

一応、オプショナルの使い方は以下の通りです。

https://camo.qiitausercontent.com/1a7af99435e5899ec36727f4f6f961fa50464e1d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f35383430342f63303735363663362d633139652d376661392d363133362d6332616539616565376162312e706e67

するとこのようになりました。

f:id:bobchan1915:20180831134810p:plain

また、self.[オブジェクト].frameとするとCGRectを渡すことになるので、CGSizeで高さと横幅だけ指定しておきます。

以下の項目を

self.passwordTextField.frame = CGRect(x: (self.view.bounds.width - formWidth) / 2, y: self.view.bounds.height / 2 - 60, width: formWidth, height: 30)

// を

self.passwordTextField.frame.size = CGSize(width: formWidth, height: 30)

に変更します。

すべて原点にあるので、以下のようになります。

f:id:bobchan1915:20180831135652p:plain

2.2 制約を課す

制約に関しての公式サイトは以下の通りです。

developer.apple.com

以下のような構成にしたいと思っています。 コンテンツはすべて中央揃えにしたいです。

f:id:bobchan1915:20180831135705p:plain

以下のように制約をかけたいオブジェクトのプロパティとしてAnchorを指定します。 また、すべてのUIViewを継承しているオブジェクトは使うことができます。

以下のようの種類を指定できます。

leadingAnchor: NSLayoutXAxisAnchor
trailingAnchor: NSLayoutXAxisAnchor
leftAnchor: NSLayoutXAxisAnchor
rightAnchor: NSLayoutXAxisAnchor
topAnchor: NSLayoutYAxisAnchor
bottomAnchor: NSLayoutYAxisAnchor
widthAnchor: NSLayoutDimension
heightAnchor: NSLayoutDimension 
centerXAnchor: NSLayoutXAxisAnchor
centerYAnchor: NSLayoutYAxisAnchor
firstBaselineAnchor: NSLayoutYAxisAnchor //UILayoutGuideにはない
lastBaselineAnchor: NSLayoutYAxisAnchor //UILayoutGuideにはない

以下の画像が参考になります。

f:id:bobchan1915:20180831141322p:plain

  • topAnchor: 上
  • leadingAnchor: 読む方向によって決まるはじまり
  • trailingAnchor: 読む方向によって決まるはじまり
  • bottomAnchor: 下
  • leftAnchor: 左
  • rightAnchor: 右

f:id:bobchan1915:20180831141418p:plain

  • centerXAnchor: X方向の中心線
  • centerYAnchor: Y方向の中心線
  • widthAnchor: 幅
  • heightAncher: 縦

medium.com

一応、補足のためにもう一つ、付け足すとすると以下の画像です。

https://developer.apple.com/jp/documentation/UserExperience/Conceptual/AutolayoutPG/Art/attributes_2x.png

そのほかにも

  • firstBaselineAnchor: テキストの最初の行のbaseline
  • lastBaselineAnchor: テキストの最後の行のbaseline

以下のような画像がわかりやすいです。

[https://cdn-images-1.medium.com/max/1600/1ky1FYn3hymW0ajz_tQ8Wow.png:image=https://cdn-images-1.medium.com/max/1600/1ky1FYn3hymW0ajz_tQ8Wow.png]

Autolayouts via Layout anchors – Hassan Ahmed Khan – Medium

また、Anchorのメソッドは

// thisAnchor = otherAnchor
func constraint(anchor: NSLayoutAnchor!) -> NSLayoutConstraint!

// thisAnchor = otherAnchor + constant
func constraint(anchor: NSLayoutAnchor!, constant c: CGFloat) -> NSLayoutConstraint!

となっています。

まず実装して見ます。 どうやらaddSubViewを追加してからじゃないと制約はかけることができないみたいです。

結果として

 // MARK: - Constrains
        
        self.imageView.widthAnchor.constraint(equalToConstant: imageViewSide).isActive = true
        self.imageView.heightAnchor.constraint(equalToConstant: imageViewSide).isActive = true
        self.imageView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        self.imageView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 120).isActive = true

        self.emailTextField.widthAnchor.constraint(equalToConstant: formWidth).isActive = true
        self.emailTextField.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        self.emailTextField.topAnchor.constraint(equalTo: self.imageView.bottomAnchor, constant: 10).isActive = true
        
        self.passwordTextField.widthAnchor.constraint(equalToConstant: formWidth).isActive = true
        self.passwordTextField.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        self.passwordTextField.topAnchor.constraint(equalTo: self.emailTextField.bottomAnchor, constant: 40).isActive = true
        
        self.button.widthAnchor.constraint(equalToConstant: formWidth).isActive = true
        self.button.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor).isActive = true
        self.button.topAnchor.constraint(equalTo: self.passwordTextField.bottomAnchor, constant: 50).isActive = true

のようにすると以下のようになります。

f:id:bobchan1915:20180901035424p:plain

2.3 制約をつける

どの制約が勝つかを定義しなければ、おなじ優先度ならば競合してしまうでしょう。

3. AutoLayoutのデバッグ

以下のサイトでデバック方法がかかれてあります。

developer.apple.com

3.1 レイアウト崩れの例

レイアウトが崩れるのは以下の二つがあります。

  • 曖昧な制約:レイアウトが決まりきらない制約
  • 制約のコンフリクト:制約の条件が衝突しているもの

3.2 曖昧な制約

以下で曖昧なものがあるかを調べられます。

self.view.hasAmbiguosLayout()

3.3 コンフリクトを確認する

以下のようにエラーが出ればコンフリクトしています。

2018-08-31 15:03:44.451155+0900 Apacheek[17051:7788013] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
    (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSAutoresizingMaskLayoutConstraint:0x60800028b7c0 h=--& v=--& UIButton:0x7f9a899756e0'SIGN IN'.midX == 150   (active)>",
    "<NSLayoutConstraint:0x60000028d2f0 UIButton:0x7f9a899756e0'SIGN IN'.centerX == UILayoutGuide:0x6000001b0680'UIViewSafeAreaLayoutGuide'.centerX   (active)>",
    "<NSAutoresizingMaskLayoutConstraint:0x60800028ce90 h=-&- v=-&- 'UIView-Encapsulated-Layout-Left' UIView:0x7f9a875172a0.minX == 0   (active, names: '|':UIWindow:0x7f9a89976620 )>",
    "<NSLayoutConstraint:0x60800028cdf0 'UIView-Encapsulated-Layout-Width' UIView:0x7f9a875172a0.width == 414   (active)>",
    "<NSLayoutConstraint:0x60000028be50 'UIViewSafeAreaLayoutGuide-left' H:|-(0)-[UILayoutGuide:0x6000001b0680'UIViewSafeAreaLayoutGuide'](LTR)   (active, names: '|':UIView:0x7f9a875172a0 )>",
    "<NSLayoutConstraint:0x60000028d570 'UIViewSafeAreaLayoutGuide-right' H:[UILayoutGuide:0x6000001b0680'UIViewSafeAreaLayoutGuide']-(0)-|(LTR)   (active, names: '|':UIView:0x7f9a875172a0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x60000028d2f0 UIButton:0x7f9a899756e0'SIGN IN'.centerX == UILayoutGuide:0x6000001b0680'UIViewSafeAreaLayoutGuide'.centerX   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

4. まとめ

特に、StoryBoardを見ながら開発していると、macbookでは小さく感じてしまうことがありました。

エンジニアとしても宣言することは非常に価値があるので、これからもコードでしっかり書いていこうと思います。

5. 次回予告

translatesAutoresizingMaskIntoConstraintsやレイアウトの階層などをまとめたいと思います。

参考文献

**以下の文献はかなり古いので、注意が必要です。

よくわかるAuto Layout iOSレスポンシブデザインをマスター

よくわかるAuto Layout iOSレスポンシブデザインをマスター