Search code examples
iosswiftui

Problems with ScrollView and fixedSize modifer in SwiftUI


I'm having an UI contains an expandable ScrollView

struct ContentView: View {
    
    var body: some View {
        
        VStack(spacing: 8) {
            Spacer()
            ScrollView {
                ExpandView()
            }
            .fixedSize(horizontal: false, vertical: true)
            Button {
                
            } label: {
                HStack {
                    Spacer()
                    Text("Add")
                    Spacer()
                }
                    .background(Color.green)
            }
        }
        .frame(maxWidth: .infinity)
        
    }
}

struct ExpandView: View {
    var body: some View {
        VStack {
            Accordion(title: "Red", content: {
                Rectangle()
                    .fill(.red)
                    .frame(height: 600)
            })
            Accordion(title: "Blue", content: {
                Rectangle()
                    .fill(.blue)
                    .frame(height: 600)
            })
        }
    }
}


struct Accordion<Content: View>: View {
    @State var isExpand: Bool = false
    let title: String
    @ViewBuilder let content: Content
    
    var body: some View {
        VStack(spacing: 0) {
            Button(action: {
                withAnimation {
                    isExpand.toggle()
                }
            }, label: {
                HStack {
                    Spacer()
                    Text(title)
                    Spacer()
                }
                
            })
            .buttonStyle(.plain)
            .frame(height: 44)
            .background(Color.yellow)
            if isExpand {
                content
                    .padding(.horizontal, 12)
                    .transition(.move(edge: .top))
            }
        }
        .clipped()
    }
}

Where user tap on Yellow Button, ScrollView will expand base on it content size. It works well if content size of ScrollView is small, but when it become larger. Button Add get pushed out of screen and can not scroll anymore.

I have try to add frame(maxHeight: ) modifer but problem still occurs. I'm expect ScrollView's frame only expand upto top safearea and not push out Button Add

Here is a gif of current bug: https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNzkxeTYxYnFicHlhcWF4d3R6aXZpdzN0MjUzYWdhbW0wODdhNnQ3ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/9D7qzWbgw79V4bY3TP/giphy.gif


Solution

  • It is almost always incorrect to put fixedSize directly on a ScrollView, because the whole point of a ScrollView is to allow its contents to overflow its own bounds so that it can be scrolled, not to have the scroll view's frame be exactly the same as its contents.

    If you want the scroll view's contents to start at the bottom, you can use defaultScrollAnchor:

    VStack(spacing: 8) {
        ScrollView {
            ExpandView()
        }
        // 'for: .alignment' can be added in iOS 18+
        .defaultScrollAnchor(.bottom/*, for: .alignment*/)
    
        // ...
    }
    

    Alternatively, use a GeometryReader and set the frame(maxHeight:), then add another frame that positions the scroll view at the bottom of the GeometryReader, akin to what your Spacer did. Spacer wouldn't work here because GeometryReader is more "greedy" than Spacer and will take up all the space instead.

    VStack(spacing: 8) {
        GeometryReader { geo in
            ScrollView {
                ExpandView()
            }
            .frame(maxHeight: geo.size.height)
            // here I set the max height before fixSize, so there is a limit as to how tall the scroll view can expand
            .fixedSize(horizontal: false, vertical: true)
            .frame(height: geo.size.height, alignment: .bottom)
        }
    }