I'm having problems laying out a VStack in SwiftUI that uses a custom pager controller, and despite trying loads of options I can't get it to behave the way I want.
Sample code:
import SwiftUI
struct ContentView: View {
@State var pageIndex: Int = 0
var body: some View {
VStack {
Text("Headline").font(.title)
Divider()
pagedView.background(Color.pink).layoutPriority(1)
Color.red
Color.blue
Color.gray
}
}
var pagedView: some View {
PagerManager(pageCount: 2, currentIndex: $pageIndex) {
VStack {
Text("Line 1")
Text("Line 2")
Text("Line 3")
Text("Line 4")
Text("Line 5")
Text("Line 6")
Text("Line 7")
Text("Line 8")
}
VStack {
Text("Line 21")
Text("Line 22")
Text("Line 23")
Text("Line 24")
Text("Line 25")
}
}
}
}
struct PagerManager<Content: View>: View {
let pageCount: Int
@Binding var currentIndex: Int
let content: Content
//Set the initial values for the variables
init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
self.pageCount = pageCount
self._currentIndex = currentIndex
self.content = content()
}
@GestureState private var translation: CGFloat = 0
var body: some View {
VStack {
singlePageView.background(Color.yellow)
HStack(spacing: 8) {
ForEach(0 ..< self.pageCount, id: \.self) { index in
CircleButton(isSelected: Binding<Bool>(get: { self.currentIndex == index }, set: { _ in })) {
withAnimation {
self.currentIndex = index
}
}
}
}
}
}
var singlePageView: some View {
GeometryReader { geometry in
HStack(spacing: 0) {
self.content.frame(width: geometry.size.width).background(Color.purple)
}
.frame(width: geometry.size.width, alignment: .leading)
.offset(x: -CGFloat(self.currentIndex) * geometry.size.width)
.offset(x: self.translation)
.animation(.interactiveSpring())
.gesture(
DragGesture().updating(self.$translation) { value, state, _ in
state = value.translation.width
}.onEnded { value in
let offset = value.translation.width / geometry.size.width
let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
}
)
}
}
}
struct CircleButton: View {
@Binding var isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: {
self.action()
}) { Circle()
.overlay(
Circle()
.stroke(Color.black,lineWidth: 1)
).foregroundColor(self.isSelected ? Color(UIColor.systemGray2) : Color.white.opacity(0.5))
.frame(width: 10, height: 10)
}
}
}
What I'm trying to achieve is for pagedView to get priority for it's own space and then the rest of the VStack views (red, blue, gray in the code) to have the remaining space divided equally between them.
Without layoutPriority(1) on pagedView the vertical space is divided equally between the 4 views (as expected), which does not leave enough space for the Text lines in the pagedView which are squashed into the bottom of the view. (Background colors added to show how the layout is working)
However, adding layoutPriority(1) on the pageView makes the embedded singlePageView (in yellow) grab all of the vertical space so the remaining Colors are not shown.
The issue seems to be somewhere in the singlePageView code, as changing singlePageView.background(Color.yellow) to content in PagerManager VStack means the layout behaves as I want (without the desired paging of course):
I know I could take a different approach and try wrapping a UIPageViewController in a UIViewControllerRepresentable but before I go down that route I'd like to know if there's a way of getting the pure SwiftUI approach to work


