1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SwiftUI で UICalendarView に小さいサイズのframeを指定すると要素が溢れる問題

Last updated at Posted at 2025-01-14

環境

  • XCode 16.1
  • Swift 5.9

遭遇した問題

SwiftUI でUICalendarViewを表示する際に、高さをframe(height:)で 200pt にした際に、以下のように画面からはみ出るということが起きました。

image.png

期待しているレイアウトは以下の様な感じです。

image.png

見比べてみるとわかるとおり、カレンダーの上部がテキストの領域に被ってしまっており見えない様になってしまっています。

問題のコードは以下です。

#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)
        }
    }
}

image.png

今回の事象で分かる通り、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)
    }
}

image.png

キャプチャを見て分かるように、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
    }
}

image.png

SampleLabelの領域が、SwiftUIで指定した高さを超えて画面外へはみ出ていることがわかります。

高さの指定を削除すると、画面外へはみ出ることはなくなります。

#Preview("画面がはみ出るサンプル") {
    VStack {
        ScrollView {
            ZStack {
                SampleLabel(text: "O")
                    .background(.red.opacity(0.3))
                Color.blue
                    .frame(width: 30, height: 200)
            }
-              .frame(maxHeight: 200)
        }
    }
}

image.png

次に、縦方向の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)
    }
}

image.png

こちらも想定通り縦方向のCompressionPriorityを下げることでSwiftUIの高さ指定が反映されたので、この事象はUIViewであれば起こりうることがわかりました。

おわり

SwiftUIでUIViewを扱うときは、UIView側でframeがどう設定されているかは意識しないと意図しないレイアウト崩れにつながりそうということがわかりました。
今回の私の遭遇したケースでは、カレンダーを見切れることはしたくなく、横幅も変えたくなかったのでsetContentCompressionResistancePriorityを使わずSwiftUI側の高さ指定を消すことで対応しましたが、その時々の状況でどういうふうに対応するかも変わってくると思うので、何ができるかを押さえた上で意思決定していければと思います。

今回の内容で他にもできることがあるなどありましたら、ご教授いただけると幸いです。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?