環境
- XCode 16.1
- Swift 5.9
遭遇した問題
SwiftUI でUICalendarView
を表示する際に、高さをframe(height:)
で 200pt にした際に、以下のように画面からはみ出るということが起きました。
期待しているレイアウトは以下の様な感じです。
見比べてみるとわかるとおり、カレンダーの上部がテキストの領域に被ってしまっており見えない様になってしまっています。
問題のコードは以下です。
#Preview("画面がはみ出るサンプル") {
VStack {
Text("Dummy Text")
ScrollView {
CalendarView(initialDate: .init(),
interval: .init(start: .now,
duration: 3000000))
.frame(maxHeight: 200)
}
}
}
struct CalendarView: UIViewRepresentable {
@Environment(\.locale) private var locale
private let initialDate: DateComponents
private let interval: DateInterval
init(initialDate: DateComponents,
interval: DateInterval) {
self.initialDate = initialDate
self.interval = interval
}
func makeUIView(context: Context) -> UICalendarView {
return UICalendarView()
}
func updateUIView(_ uiView: UICalendarView, context: Context) {
uiView.visibleDateComponents = initialDate
uiView.availableDateRange = interval
uiView.locale = locale
}
}
問題の原因を考える
問題のコードから高さの指定を削除すると、問題が解消されたことから指定したサイズとCalendarView
自体のサイズと競合して発生した問題推測できます。
#Preview("画面がはみ出るサンプル") {
VStack {
Text("Dummy Text")
ScrollView {
CalendarView(initialDate: .init(),
interval: .init(start: .now,
duration: 3000000))
- .frame(maxHeight: 300)
}
}
}
CalendarView
の上に同じ高さで指定したRectangle
を配置してみるとCalendarView
が指定したサイズである200ptを超えたサイズで表示されてしまっていることがわかります。
#Preview("画面がはみ出るサンプル") {
VStack {
Text("Dummy Text")
ScrollView {
ZStack {
CalendarView(initialDate: .init(),
interval: .init(start: .now,
duration: 3000000))
.background(.red.opacity(0.3))
Rectangle()
.frame(width: 30, height: 200)
}
.frame(maxHeight: 200)
}
}
}
今回の事象で分かる通り、UICalendarView
のサイズとSwiftUIの高さ指定が競合した場合、UICalendarView
のサイズが優先されるため、SwiftUIを指定しない様にすれば事象は解決します。
UICalendarView
の高さをSwiftUI側で指定した値を優先して反映してほしい場合
以下記事で解説されていますが、UICalendarView
に対してsetContentCompressionResistancePriority
というAPIを用いれば可能です。
APIのドキュメントは以下です。
Sets the priority with which a view resists being made smaller than its intrinsic size.
日本語訳
ビューが本来のサイズよりも小さくなるのを防ぐ優先度を設定します。
ドキュメントの記載にある通り、setContentCompressionResistancePriority
ではViewが小さくなるのを防ぐ優先度を設定するもので、今回の場合は縦方向に小さくなるのを防ぐ優先度が高いため起きた事象と思われるので、この優先度を下げることでうまくいきそうです。
Preview("画面がはみ出るサンプル") {
VStack {
Text("Dummy Text")
ScrollView {
ZStack {
CalendarView(initialDate: .init(),
interval: .init(start: .now,
duration: 3000000))
.background(.red.opacity(0.3))
Rectangle()
.frame(width: 30, height: 200)
}
.frame(maxHeight: 200)
}
}
}
struct CalendarView: UIViewRepresentable {
@Environment(\.locale) private var locale
private let initialDate: DateComponents
private let interval: DateInterval
init(initialDate: DateComponents,
interval: DateInterval) {
self.initialDate = initialDate
self.interval = interval
}
func makeUIView(context: Context) -> UICalendarView {
return UICalendarView()
}
func updateUIView(_ uiView: UICalendarView, context: Context) {
uiView.visibleDateComponents = initialDate
uiView.availableDateRange = interval
uiView.locale = locale
+ uiView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
}
}
キャプチャを見て分かるように、CalendarView
はSwiftUIの指定通り高さ200ptで表示されるようになり上部が見切れてしまうこともなくなりましたが、今度は下部が見切れてしまいました。
明確に記載されている記事が見つからなかったので事象からの推測になりますが、UICalendarView
を全て表示するには最低限必要な高さがあるため、UICalendarView
のCompressionPriority(小さくなることを防ぐ優先度)よりも高い優先度で小さい高さを指定された場合には見切れて表示するしか無くなるのかなと思いました。
カレンダーが全て表示されないのは、望むところではないと思うので、高さの指定はしない方が良いのかなというのが、触ってみての現場の私の結論です。
これってUICalendarView
以外でも起きるものなの?
今回の原因を考えると、UICalendarView
というよりはUIView
のCompressionPriorityの概念によるものに依存していそうなのでUIView
であれば他のコンポーネントでも起きそうだなと思いました。
ですので、試しにUILabel
で同じことが起きるか見てみます。
#Preview("画面がはみ出るサンプル") {
VStack {
ScrollView {
ZStack {
SampleLabel(text: "O")
.background(.red.opacity(0.3))
Color.blue
.frame(width: 30, height: 200)
}
.frame(maxHeight: 200)
}
}
}
struct SampleLabel: UIViewRepresentable {
let text: String
func makeUIView(context: Context) -> UILabel {
return UILabel()
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.font = .systemFont(ofSize: 400, weight: .bold)
uiView.text = text
}
}
SampleLabel
の領域が、SwiftUIで指定した高さを超えて画面外へはみ出ていることがわかります。
高さの指定を削除すると、画面外へはみ出ることはなくなります。
#Preview("画面がはみ出るサンプル") {
VStack {
ScrollView {
ZStack {
SampleLabel(text: "O")
.background(.red.opacity(0.3))
Color.blue
.frame(width: 30, height: 200)
}
- .frame(maxHeight: 200)
}
}
}
次に、縦方向のCompressionPriorityを下げてみて、再度SwiftUI側で高さを指定した時に、SwiftUIの高さが優先され、UIView側のコンテンツが見切れるとなると今回の仮説(UICalendarView
だけでなくUIView
であれば今回の事象は発生する)は正しそうです。
#Preview("画面がはみ出るサンプル") {
VStack {
ScrollView {
ZStack {
SampleLabel(text: "O")
.background(.red.opacity(0.3))
Color.blue
.frame(width: 30, height: 200)
}
+ .frame(maxHeight: 200)
}
}
}
struct SampleLabel: UIViewRepresentable {
let text: String
func makeUIView(context: Context) -> UILabel {
return UILabel()
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.font = .systemFont(ofSize: 400, weight: .bold)
uiView.text = text
+ uiView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
}
}
こちらも想定通り縦方向のCompressionPriorityを下げることでSwiftUIの高さ指定が反映されたので、この事象はUIView
であれば起こりうることがわかりました。
おわり
SwiftUIでUIViewを扱うときは、UIView側でframeがどう設定されているかは意識しないと意図しないレイアウト崩れにつながりそうということがわかりました。
今回の私の遭遇したケースでは、カレンダーを見切れることはしたくなく、横幅も変えたくなかったのでsetContentCompressionResistancePriority
を使わずSwiftUI側の高さ指定を消すことで対応しましたが、その時々の状況でどういうふうに対応するかも変わってくると思うので、何ができるかを押さえた上で意思決定していければと思います。
今回の内容で他にもできることがあるなどありましたら、ご教授いただけると幸いです。