I have reached an annoying issue with SwiftUI. I have a horizontal pager with vertical scroll views as pages. It is defined as simple as they come,
TabView(selection: $selected) {
ForEach(focus!.list.things) { thing in
FullView(thing: thing).tag(thing)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
and
struct FullView: View {
let thing: Thing
var body: some View {
ScrollView {
VStack {
...
}
}
}
}
This produces a view which does what I want, except it does not reach all the way down below the home indicator.
I can solve this by adding .ignoresSafeArea(edges: .bottom)
to the TabView
, but that produces another displeasing result where the page indicator collides with the home indicator.
Is there any reasonable way accomplish full height vertical scroll while keeping the index page indicator above the home indicator?
Code to recreate issue:
struct ContentView: View {
@State var isSheetUp = false
var body: some View {
Button("Present") {
isSheetUp.toggle()
}
.sheet(isPresented: $isSheetUp) {
Sheet()
}
}
struct Sheet: View {
var body: some View {
NavigationView {
TabView() {
Page()
Page()
Page()
}
// Comment this to switch layout issue
.ignoresSafeArea(edges: .bottom)
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
.navigationTitle("Title")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct Page: View {
var body: some View {
ScrollView {
VStack {
Rectangle()
.foregroundColor(.teal)
.padding()
.frame(minHeight: 10000)
}
}.background(Color.brown)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
2
Answers
EDIT: See also @nekno’s fantastic additions!
This is possible if you create a custom
UIPageControl
, manually tag each tab in theTabView
, and make sure to keep track of thenumberOfPages
:@Coder-256’s answer set me on the right path, and I added a couple enhancements you might find useful.
The
UIPageControl
normally iterates through the pages when you tap on it. As written, the indicator in the page control was changing, but the pages weren’t actually changing, so I added a target for the page control’s.valueChanged
event.When setting the current page based on the new changed value, wrapping the assignment in a
withAnimation
closure ensure the page animates to the next page, otherwise it just replaces the current page instantaneously.TabView
will work with any validtag
values, which just need to conform toHashable
.To work with the page control, you need those tag values to be convertible to
Int
values, but it’s common practice to use a strongly-typed, named value for tags, so I added support for anenum
that conforms toRawRepresentable
with a backing type ofInt
.Others may find it easier to just use hard-coded integers for the tag values, so if you ever reordered the pages in your
TabView
you wouldn’t have to remember to reorder the cases in yourenum
, but to each their own.The
UIPageControl
and its parentViewHost
that hosts theUIViewRepresentable
instance both have auto resizing masks that result in their frames expanding to consume the horizontal space of the containing superview.Both the page control and the view host participate in hit testing, so they intercept touches to the left and right of the page control when you actually intend to scroll the content underneath.
Adding the
allowsHitTesting(false)
view modifier eliminates that behavior, but also disables all interaction with the page control, so it breaks the tap/paging functionality.I played around with various solutions, and the easiest seems to be to just set a frame on the page control that requests a
maxWidth
andmaxHeight
of0
, and as a result the view shrinks to its intrinsic content size.