[iOS]枠線の上にViewを乗せようとしたらviewとlayerの重なり順に苦しんだ話
Viewに枠線をつけて、その上にViewを乗せるコードを書いていたらUIViewとCALayerの重なり順に手間取ったのでメモ的に記事にします。
souce code
https://gist.github.com/SatoTakeshiX/1b9050a7ea517785e3b45ace0a38a5ac
実行環境
- Xcode 13
- iOS 15.0
- Swift 5.5
(とはいえ、内容はiOS 15でなくても動きます)
枠線の上にViewを表示したい
UIView
に枠線をつけて、その上にバッチのようにUIImageView
を乗せようとしました。
ちょうど次の図のようなレイアウトです。
最初の方針として「UIView
のlayer
プロパティで枠線をつけて、その上にUIImageView
を表示するAuto Layoutを設定すればよいだろう」と思っていました。
そこでこんなコードを実装しました。
Signboard
というViewに緑の枠線を表示するboardView
とその上に王冠のImage ViewであるcrownImageView
を乗せるコードです。
class Signboard: UIView {
init() {
super.init(frame: .zero)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
addSubview(boardView)
boardView.topAnchor.constraint(equalTo: topAnchor).isActive = true
boardView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
boardView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
boardView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
boardView.addSubview(crownImageView)
crownImageView.topAnchor.constraint(equalTo: boardView.topAnchor, constant: -20).isActive = true
crownImageView.centerXAnchor.constraint(equalTo: boardView.centerXAnchor).isActive = true
}
// 緑の枠線
private lazy var boardView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderWidth = 2
view.layer.borderColor = UIColor.systemGreen.cgColor
view.layer.cornerRadius = 4
return view
}()
// 王冠のImage View
private lazy var crownImageView: UIImageView = {
let imageView = UIImageView(image: .init(systemName: "crown"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 40).isActive = true
imageView.backgroundColor = .systemRed
return imageView
}()
これでいけるかと思ったら実際の表示はこちら。
枠線の緑のほうが王冠のImage Viewより上に表示されてしまいます。
どうやらview.layer
で枠線を作ると一番最後のレイヤーとして表示されてしまうようです。
この記事ではUIViewもCALayerも追加した順に表示されるそうです。
view.layer
自体は最後に追加することになるんでしょうか?🤔
UIViewとCALayerの階層構造 - Qiita
新しくCALayerをつくりaddSublayerする
view.layer
は使わず、新しくCALayer
を作成してaddSubLayer
するようにします。
private func setup() {
addSubview(boardView)
boardView.layer.addSublayer(borderLayer)
}
private lazy var boardView: UIView = {
let view = BoardView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var borderLayer: CALayer = {
let layer = CALayer()
layer.borderWidth = 2
layer.borderColor = UIColor.systemGreen.cgColor
layer.cornerRadius = 4
return layer
}()
ただしborderLayer
のCALayer
のframe
プロパティはaddSublayer
した後もViewに追従するわけではありません。開発者が設定する必要があります。
今回はboardView
のbounds
と同じ値を入れたいのでboardView
のサブクラスを作ることにしました。
BoardView
です。
UIViewのlayoutSubviews
をオーバーライドしてbounds
の情報をクロージャーで外に通知します。
final class BoardView: UIView {
var didLayoutSubView: ((CGRect) -> Void)?
init() {
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// サブビューのレイアウトが終わったら呼ばれる
override func layoutSubviews() {
super.layoutSubviews()
// クロージャーで外に通知
didLayoutSubView?(bounds)
}
}
Signboard
に戻り、boardView
の型をUIViewからBoardViewに変えます。
boardView
のlayoutSubView
が終わったらframe
をborderLayer
に更新します。
// Signboard
private func setup() {
addSubview(boardView)
boardView.layer.addSublayer(borderLayer)
boardView.didLayoutSubView = { [weak self] bounds in
// layerのframe情報をboardViewのもので更新
self?.borderLayer.frame = bounds
}
}
// 継承の型をUIViewからBoardViewに変える
private lazy var boardView: BoardView = {
let view = BoardView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
このように書き換えることで無事枠線よりも上に王冠のImage Viewが表示されるようになります。
まとめ
UIView
のlayer
プロパティでレイアウトすると、addSubView
した他のViewとの表示順がおかしくなります。
別途CALayer
作ってaddSublayer
しましょう。
CALayer
の矩形情報は自分で設定必要なのでUIViewのlayoutSubviews
で矩形情報を取得しましょう。
参考URL
- UIViewとCALayerの階層構造 - Qiita
- CALayer And Auto Layout With Swift - MarcoSantaDev
- CALayerは自動でUIViewのサイズに追従しない
- 追従する方法は3つ
- UIViewControllerの
viewDidLayoutSubviews
を使うか、UIViewのlayoutSubviews
を使う - KVOでViewのbounds更新を購読する
- カスタムViewを作って
layerClass
をオーバーライドする
- UIViewControllerの
宣伝
インプレスR&D社より、「1人でアプリを作る人を支えるSwiftUI開発レシピ」発売中です。
「SwiftUIでアプリを作る!」をコンセプトにSwiftUI自体の解説とそれを組み合わせた豊富なサンプルアプリでどんな風にアプリ実装すればいいかが理解できる本となっています。
iOS 14対応、Widgetの作成も一章まるまるハンズオンで解説しています。
SwiftUIを学びたい方、ぜひこちらのリンクをチェックしてください!
https://nextpublishing.jp/book/12491.html
また、Visionレームワークの入門書も発売中。
iOSで画像分析に興味のある方は是非チェック!