Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is causing my SwiftUI view with higher layout priority to grab all the space?

Tags:

swiftui

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)

Without layoutPriority(1)

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.

With layoutPriority(1)

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):

enter image description here

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

like image 499
Fleet Phil Avatar asked Dec 07 '25 05:12

Fleet Phil


1 Answers

I'm not sure if your question is still an open issue for you, but for everybody else with a similar problem, the following explanation might help:

By using the layoutPriority modifier, you are asking SwiftUI to start laying out the body views of the ContentViews VStack with the singlePageView. This means that this view now gets the full available space offered. As you use the GeometryReader as your outermost view, it will claim the maximum space available no space leaving to all the remaining views. GeometryReader is like Color a view which will always occupy the maximum available space offered by the parent view. (For more information about the SwiftUI layout process, read for example How layout works in SwiftUI from HackingWithSwiftUI.)

If you remove the layoutPriority modifier, all the views are equally important to the parent and therefore each of them will get a quarter of the available space. This means for your singlePageView that it is not enough to show it's full content: The GeometryReader could only claim a quarter of the available height.

Your main goal is, that the PagerManager is occupying the full horizontal screen space available but uses only the minimum required vertical space. You should therefore not use the GeometryReader as your main view in singlePageView as this will influence how its content will be laid out. But nevertheless you have to size the content of the page view to occupy the full width.

What can you do get your desired layout?

My solution:

  1. Introduce a state variable pageWidth in PagerManager:
@State private var pageWidth: CGFloat = 600
  1. Measure the screen width using GeometryReader on the background of the circle buttons parent HStack (the .frame(maxWidth: .infinity) modifier ensures that it occupies the whole available screen width):
HStack(spacing: 8) {
    ForEach(0 ..< self.pageCount, id: \.self) { index in
        // ... omitted CircleButton code
    }
}
// Measure the width of the screen using GeometryReader on the background
.frame(maxWidth: .infinity)
.background(
    GeometryReader { proxy -> Color in
        DispatchQueue.main.async {
            pageWidth = proxy.size.width
        }
        return Color.clear
    }
)
  1. Then in singlePageView remove the GeometryReader and replace all occurrences of geometry.size.width with pageWidth:
var singlePageView: some View {
    HStack(spacing: 0) {
        self.content.frame(width: pageWidth).background(Color.purple)
    }
    .frame(width: pageWidth, alignment: .leading)
    .offset(x: -CGFloat(self.currentIndex) * pageWidth)
    .offset(x: self.translation)
    .animation(.interactiveSpring())
    .contentShape(Rectangle())  // ensures we can drag on the transparent background
    .gesture(
        DragGesture().updating(self.$translation) { value, state, _ in
            state = value.translation.width
        }.onEnded { value in
            let offset = value.translation.width / pageWidth
            let newIndex = (CGFloat(self.currentIndex) - offset).rounded()
            self.currentIndex = min(max(Int(newIndex), 0), self.pageCount - 1)
        }
    )
}

As you can see I've added .contentShape(Rectangle()) modifier to ensure that the DragGesture also works on the empty background.

like image 190
pd95 Avatar answered Dec 11 '25 08:12

pd95



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!