Reputation: 277
How do I make my SwiftUI TabView
with a PageTabViewStyle
adjust its height to the height of the content?
I have a SwiftUI
view like follows:
struct TabViewDynamicHeight: View {
var body: some View {
VStack {
TabView {
ForEach(0..<5, id: \.self) { index in
VStack {
Text("Text \(index)")
Text("Text \(index)")
Text("Text \(index)")
}
}
}
.tabViewStyle(PageTabViewStyle())
.background(Color.red)
.fixedSize(horizontal: false, vertical: true)
}
.background(Color.blue)
}
}
This produces an output like this:
You can see, that the content of the TabView
is cut off. I'm aware, that I can remove .fixedSize
, but than the view looks like this:
I would like the TabView to respond to the height of the content. Any ideas on how to achieve that?
Upvotes: 14
Views: 4384
Reputation: 912
I found a solution which works fine with accessibility.
Statically define a font for your texts
define height for each ContentSizeCategory
(if you are using system fonts, you can use Typography specifications from Apple HIG.
Dynamically generate a UIFont
using UIFont.withSize(_)
from ContentSizeCategory
retrieved using @Environment(\.sizeCategory)
Display text using with a SwiftUI Font
based on UIFont
Use GeometryProxy
to retrieve width of your screen.
From screen width, you can calculate remaining width for your text based on margins and components size
You can calculate minimum height with margins and components around your text
With CoreText
and ContentSizeCategory
, you can calculate height of each text by using NSAttributedString
, CTFramesetterCreateWithAttributedString()
and CTFramesetterSuggestFrameSizeWithConstraints()
For each item that will be displayed on your TabView
, there is an height that can be calculated from @Environment(\.sizeCategory)
and GeometryProxy
. And you can also calculate the maximum height of all items.
Apply the maximum height for all views inside TabView
Enjoy 😉
Upvotes: 0
Reputation: 713
I took inspiration from Asperi' answer, however I had to modify some things to make it work in my case.
On the 'Content' of the TabView, I added these:
.overlay(GeometryReader { proxy in
Color.clear.preference(key: ViewRectKey.self, value: proxy.size)
})
.onPreferenceChange(ViewRectKey.self) { size in
if self.viewPagerSize.height == .zero {
self.viewPagerSize = size
}
}
Where viewPagerSize is initially:
@State var viewPagerSize: CGSize = .zero
My PreferenceKey looks like this:
struct ViewRectKey: PreferenceKey {
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
In total the code looks like this:
@ViewBuilder
func createViewPager() -> some View {
TabView {
ForEach(views(), id: \.id) { _ in
createInformationSquareView()
.background(Color(.systemGray6))
.overlay(GeometryReader { proxy in
Color.clear.preference(key: ViewRectKey.self, value: proxy.size)
})
.onPreferenceChange(ViewRectKey.self) { size in
if self.viewPagerSize.height == .zero {
self.viewPagerSize = size
}
}
}
}
.cornerRadius(10)
.frame(height: self.viewPagerSize.height + 60)
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.padding()
}
Upvotes: 0
Reputation: 257663
A possible approach is to fetch content rect dynamically in run-time and transfer to parent via view prefs, so parent can set it as frame to fit content.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
VStack {
Text("Text \(index)")
Text("Text \(index)")
Text("Text \(index)")
}
.frame(maxWidth: .infinity)
.background(GeometryReader {
Color.clear.preference(key: ViewRectKey.self,
value: [$0.frame(in: .local)])
})
// ...
.frame(height: rect.size.height
+ 60 /* just to avoid page indicator overlap */)
.onPreferenceChange(ViewRectKey.self) { rects in
self.rect = rects.first ?? .zero
}
Complete test code in project is here
Upvotes: 4