iosswiftuiswiftui-listswiftui-view

Is it possible to have a list style input TextField stay frozen at the top while a list of data below it is able to scroll?


The following is a simplified version of a screen that is in my app:

enter image description here

From the following code:

import SwiftUI

struct HomeView: View
{
    @State private var textFieldUserInput: String = ""

    var body: some View
    {
        VStack
        {
            List
            {
                Section
                {
                    TextField("Enter Text", text: self.$textFieldUserInput)
                }
                
                Section
                {
                    Text("Example Text 1")
                    Text("Example Text 2")
                    Text("Example Text 3")
                }
            }
        }
    }
}

I was wanting to have the second Section being able to scroll, while the TextField in the first Section is unable to scroll.

When set I .scrollDisabled to true on the List, and put the second Section in a ScrollView, it works, but it completely changes the formatting and style of the second Section.

Similarly, when I set .scrollDisabled to false and move the Section with the TextField outside of the List, it also makes it look completely different. If I try wrapping the TextField in another List while it's outside of the original List, it maintains the same look, but then each List takes up half of the screen (the second Section doesn't appear directly under the TextField anymore).

I'm just wondering if it's possible to maintain the exact look and style as I have in the above image while enabling the Section below the TextField to be scrollable while the TextField itself is unable to scroll.


Solution

  • It would be quite easy to copy the styling that you get when the TextField is inside a List and just apply it to a field outside the list. But if you want to keep the field inside a List, your idea of wrapping the field in a separate List will work. You just need to set a maximum height on the List. You might also want to set the .contentMargins, to reduce the amount of space above and below the lists.


    With iOS 18, you can actually measure the height of the List content using .onScrollGeometryChange and then apply this as max height to the List:

    @State private var maxTopListHeight: CGFloat?
    
    VStack(spacing: 0) {
        List {
            TextField("Enter Text", text: $textFieldUserInput)
        }
        .frame(maxHeight: maxTopListHeight, alignment: .top)
        .contentMargins(.top, 20)
        .onScrollGeometryChange(for: CGFloat.self, of: \.contentSize.height) { _, height in
            maxTopListHeight = height
        }
        List {
            Section {
                ForEach(1..<100) { i in
                    Text("Example Text \(i)")
                }
            }
        }
        .contentMargins(.top, 0)
    }
    

    For older iOS versions, you could define the maximum height as a ScaledMetric instead, so that it adapts to dynamic font sizes.

    The value doesn't have to be too exact because any excess space below the text field is filled by the List with the group background color. So the configured height just needs to exceed the height of the field itself + top content margin.

    @ScaledMetric(relativeTo: .title) private var maxTopListHeight = 80.0
    
    VStack(spacing: 0) {
        List {
            TextField("Enter Text", text: $textFieldUserInput)
        }
        .frame(maxHeight: maxTopListHeight, alignment: .top)
        .contentMargins(.top, 20)
        List {
            Section {
                ForEach(1..<100) { i in
                    Text("Example Text \(i)")
                }
            }
        }
        .contentMargins(.top, 0)
    }
    

    Animation