AutoLayout を使って苦戦した箇所
AutoLayout を使う機会があったので、よく分かる Auto Layout
を読みながら開発を行いました。
普段からStoryBoard等は使わないで開発を行っているため、SnapKit
というライブラリを使用しました。
苦戦した箇所はいろいろあるのですが、覚えている範囲でいくつかまとめます。
トルツメ
UIStackViewを使うとトルツメも簡単に実現できます。いくつかのViewをUIStackViewに追加していき、必要なときに非表示にしたいViewのisHidden
をtrue
にするだけでトルツメされます。
レイアウトを組む上で、特に問題なければ(対応OSバージョンが9以上で良いなど)、UIStackViewを使うと楽ですね。
[label1, label2, label3].forEach(stackView.addArrangedSubview) stackView.axis = .horizontal stackView.spacing = 10 stackView.distribution = .fillEqually stackView.snp.makeConstraints { (make) in // add constraints } ... // if necessary label2.isHidden = true
UIStackViewを使わないトルツメの方法もいくつかありますが、自分が苦戦したのは優先度を変更する方法でした。
まず優先度についてですが、AutoLayoutの優先度は1 ~ 1000までの値を設定できます。何も設定しない場合はUILayoutPriorityRequired(1000)
が設定されます。
制約が重複していた場合は、優先度が高い順に満たされていきます。
このUILayoutPriorityRequired
が設定された制約は、別の優先度に変更することができません。NSInternalInconsistencyException
の例外が投げられます。
優先度を変更する必要がある場合は999
以下の値を使用するか、UILayoutPriorityDefaultHigh
やUILayoutPriorityDefaultLow
を使用することで解決できます。
トルツメの話に戻りますが、下記のように優先度を指定したConstraint
を保持しておき、必要な箇所で優先度を変更することでトルツメが実現できます。
3つのUILabelを表示中に、1つのLabelを非表示にした時にトルツメします。具体的には、label6
の左端を決める制約を2つ用意し、それぞれ異なる優先度にしておきます。
優先度の高い制約が優先されるので、必要に応じて優先度を切り替えることでレイアウトを変えています。この優先度に1000
を設定していたため、どのように実装しても優先度を変更することができず、はまりました。AutoLayoutをそのまま使っている場合は例外が投げられてクラッシュしてしまうので気づきやすいのですが、SnapKitを使っていると優先度の値が変わらないだけだったので、余計にはまってしまいました。
[label4, label5, label6].forEach(view.addSubview) label4.snp.makeConstraints { (make) in // add constraints } label5.snp.makeConstraints { (make) in // add constraints } label6.snp.makeConstraints { (make) in // add constraints label6Constraint1 = make.left.equalTo(label5.snp.right).offset(10).priority(UILayoutPriorityDefaultHigh).constraint label6Constraint2 = make.left.equalTo(label4.snp.right).offset(10).priority(UILayoutPriorityDefaultLow).constraint } // if necessary label5.isHidden = true label6Constraint1?.update(priority: label5.isHidden ? UILayoutPriorityDefaultLow : UILayoutPriorityDefaultHigh) label6Constraint2?.update(priority: label5.isHidden ? UILayoutPriorityDefaultHigh : UILayoutPriorityDefaultLow)
複数オブジェクトの中央揃え
複数のViewをまとめたカスタムビューを画面中央に配置しつつ、外枠をつけたり背景色をつけたりするときにちょっと悩みました。
イメージとしては、UIButtonに画像とテキストを配置した状態はどのように実装するのか、といった感じです。
中央揃え自体は、centerX
やcenterY
を使用することでなんとかなるのですが、カスタムビュー自体のサイズが0になってしまっていました。なかなか苦戦しました。
実装した際は、intrinsicContentSize
を使うことで解決しました。これは固有のサイズを返すもので、Viewのサイズを計算して返すことでそのView自体のサイズを知ることが出来ます。例えばUILabelのテキストの長さに応じて可変になるViewであれば、そのViewのintrinsicContentSize
でUILabelのintrinsicContentSize
を使った値を返してやることでUILabelが収まるサイズを知ることが出来ます。
実際にはmarginや固定のwidthを指定している箇所もあるため、それらをきちんと管理しないと面倒なことになりそうです。このあたりなんとかしたい。
private let view = UIView() private let label = UILabel() private var fixWidth: CGFloat = 0.0 private var fixHeight: CGFloat = 0.0 override var intrinsicContentSize: CGSize { return CGSize(width: fixWidth + label.intrinsicContentSize.width, height: fixHeight) } override init(frame: CGRect) { super.init(frame: frame) [view, label].forEach(addSubview) view.backgroundColor = .black view.snp.makeConstraints { (make) in make.size.equalTo(20) make.centerY.equalToSuperview() make.left.equalToSuperview().inset(10) } fixWidth += 20 + 10 // view.width + padding fixHeight += 20 // view.height label.text = "Label" label.snp.makeConstraints { (make) in make.left.equalTo(view.snp.right).offset(10) make.centerY.equalToSuperview() } fixWidth += 10 // margin fixWidth += 10 // padding }
ただ、後から考えてみるとこんなことをする必要はありませんでした。上記コードだと、label
に右端の制約を付けてやることで、intrinsicContentSize
は必要なくなります。かなりコードがすっきりします。StoryBoard等でAutoLayoutを使用していれば気づいたかもしれないですね。
まあ、ここでのintrinsicContentSize
の知識が後述のUICollectionViewで活きたような気がするのですが、これももっと考えればうまく実装できそうです。
label.snp.makeConstraints { (make) in make.left.equalTo(view.snp.right).offset(10) make.right.equalToSuperview().inset(10) make.centerY.equalToSuperview() }
CollectionView の Self-Sizing Cells
UITableView の Self-Sizing Cells
は非常に便利です。自動でセルの高さを調整してくれる機能ですね。
これをUICollectionViewで使おうとしたのですが、iOS10以上でないと使えないとのことでした。
結局これも、intrinsicContentSize
でサイズを計算してやり、それをCollectionViewに返してやるようにして対処しました。このあたりももう少しスマートに書けたらなと思いますね。
幅に合わせてマージンを詰める
デザインをあてはめたレイアウトを組んでいき、後から「iPhone5とかで表示させると幅が狭くなる」ことに気づき、微妙なマージンの調整などをすることがありました。
例えば、デザイン通りでれば10ptのマージンが、iPhone5の場合は5ptくらいにしたい、など。
下記のような感じで、less than or equal
を指定することで解決できました。
space.snp.makeConstraints { (make) in // add constraints make.width.equalTo(10).priority(UILayoutPriorityDefaultHigh) make.width.lessThanOrEqualTo(10).priority(UILayoutPriorityDefaultLow) ... }
まとめ
AutoLayoutは非常に便利だと感じました。AutoLayoutを使う以前は、init
やviewDidLoad
で初期化などを行いlayoutSubviews
等でレイアウトをごにょごにょしていましたが、AutoLayoutを使うようになってからは初期化と同じタイミングで制約をつけるだけなので、コードが見やすくなった気がします。ライブラリのおかげもあると思いますが。
iPhone Xも発表されましたし、UIStackViewなど、今後もAutoLayoutを使う上で便利なコンポーネントや機能が増えていくのかなと思います。使いこなしていきたいです。
ちなみに、業務でもたまにAndroidを実装することがあるのですが、AndroidのConstraintLayout
も気になっています。時間があるときに勉強してみようと思います。