Search code examples
swiftforeachuiscrollviewswiftui

SwiftUI - unexpected behavior with the frame of a ScrollView


I was working with a ForEach inside of a ScrollView, which has a frame to ensure it is the correct size.

Here is a minimum working example I created (leaving all modifiers, as they might be relevant):


import SwiftUI

struct FirstView: View {
    
    let screenWidth = UIScreen.main.bounds.width
    let screenHeight = UIScreen.main.bounds.height
    @State var items: [String] = []
    var body: some View {
        VStack{
            HStack{
                ScrollView{
                    Text("Items")
                        .font(.caption)
                    ForEach(self.items, id: \.self) { item in
                        Text(item)
                            .bold()
                            .font(.footnote)
                            .fixedSize(horizontal: false, vertical: true)
                            .lineLimit(5)
                            .frame(width: self.screenWidth * 0.27)
                            .padding(5)
                            .background(Color(red: 85/255, green: 91/255, blue: 2/255))
                            .cornerRadius(10)
                            .foregroundColor(.white)
                            .padding(4)
                            .overlay(
                                RoundedRectangle(cornerRadius: 10)
                                    .stroke(Color(red: 21/255, green: 234/255, blue: 53/255), lineWidth: 3)
                        )
                            .padding(4)
                    }
                }
                .frame(width: screenWidth * 0.3, height: screenHeight * 0.12)
                .padding(2)
                .overlay(
                    RoundedRectangle(cornerRadius: 10)
                        .stroke(Color.black, lineWidth: 2)
                )
            }
            Button(action: {
                self.items.append("item")
            }){
                Text("add item")
            }
        }
    }
    
}

struct FirstView_Previews: PreviewProvider {
    static var previews: some View {
        FirstView()
    }
}

With this example, I expected each item inside of the ForEach loop to be the same width as the ScrollView itself. This is the case, if you start the view with items already inside the items array. For instance, if I begin with @State var items: [String] = ["item1", "item2"] I get something like this: result

which is exactly what you would expect.

If I press the "add item", items are added as expected.

However if I begin with an empty items array (as in the code below), and then press the add item button, we get something like this: enter image description here

Which is not at all what is expected. The entire item is there, and we can even scroll horizontally inside of the ScrollView to see the entire item, but it appears the frame of the ScrollView is not the correct width.

I've tried adding a few frame modifiers here and there to try and resolve this issue, but cannot find a solution.

If anyone has seen this problem before, or knows what the issue is/how I can work around it, it would be much appreciated.


Solution

  • With SwiftUI 2.0 all works fine as-is

    SwiftUI 1.0+

    With empty ScrollView they did not detect complete possible width (because dynamic part is empty), so width was taken by maximum available child (in your case it is "Items" title) or default minimum if there is no-one inside.

    The solution is to embed dynamic content into some container and give it maximum available width of scroll view itself.

    Tested with Xcode 11.4 / iOS 13.4

    ScrollView{
        Text("Items")
            .font(.caption)
        VStack {
            ForEach(self.items, id: \.self) { item in
                Text(item)
                    .bold()
                    .font(.footnote)
                    .fixedSize(horizontal: false, vertical: true)
                    .lineLimit(5)
                    .frame(maxWidth: .infinity)
                    .padding(5)
                    .background(Color(red: 85/255, green: 91/255, blue: 2/255))
                    .cornerRadius(10)
                    .foregroundColor(.white)
                    .padding(4)
                    .overlay(
                        RoundedRectangle(cornerRadius: 10)
                            .stroke(Color(red: 21/255, green: 234/255, blue: 53/255), lineWidth: 3)
                )
                .padding(4)
            }
        }
        .frame(maxWidth: .infinity)
    }