'contentInset on SwiftUI's List

Is there a way how to set contentInset on SwiftUI's List? I need to adjust the bottom inset to have the last item visible above the bottom button.

enter image description here

My current solution is the following

Section(header: Color.clear.frame(height: 64)) { EmptyView() }

but I wonder is there a better way?



Solution 1:[1]

My solution to your problem looks similar with Asperi's answer but his answer will not show the Button at the top of the view since the Button is at the last section's footer in the Form. (Button will not be visible if you don't scroll to bottom.)


struct ContentView: View {
  
  @Environment(\.colorScheme) var colorScheme
  
  let stringArray: [String] = [String](repeating: "Example", count: 30)
  
  var body: some View {
    ZStack(alignment: .bottom) {
      List {
        ForEach(0..<stringArray.count) { stringIndex in
          Section {
            Text(stringArray[stringIndex])
          }
          
          if  stringIndex == stringArray.count - 1 {
            Spacer()
              .listRowInsets(EdgeInsets())
              .listRowBackground(
                colorScheme == .light ?
                  Color(.secondarySystemBackground) :
                  Color(.systemBackground)
              )
          }
        }
      }
      .listStyle(InsetGroupedListStyle())
      
      Button(action: {}) {
        Label("ADD NEW ITEM", systemImage: "plus")
      }
      .foregroundColor(.white)
      .font(.headline)
      .frame(height: 64)
      .frame(maxWidth: .infinity)
      .background(Color.red)
      .cornerRadius(5)
      .padding(.horizontal)
    }
  }
}

enter image description here

Solution 2:[2]

For a floating button, you can use .safeAreaInset on the List. The button will stay pinned to the bottom and the list will scroll and give it appropriate padding.

List {
    ForEach((0...20), id: \.self) { row in
        Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit")
    }
}
.listStyle(.inset)
.safeAreaInset(edge: .bottom) {
    Button {
        //Button Action
    } label: {
        Text("Button")
            .font(.title3)
            .bold()
            .foregroundColor(.white)
            .padding()
            .background(Color.red)
            .cornerRadius(16)
    }
}

List inset at bottom with floating button

Solution 3:[3]

Possible simple solution is just to use footer of last Section.

Below is a standalone demo (of course styles you can tune as you need). Tested with Xcode 12.4 / iOS 14.4

demo

struct DemoButtonInLastSection: View {
    let data = Array(repeating: "some", count: 20)

    var body: some View {
      Form {
        ForEach(data.indices, id: \.self) { i in
          Section(footer:
            Group {
                if i == data.count - 1 {
                    Button(action: {}) {
                        RoundedRectangle(cornerRadius: 8)
                            .overlay(
                              Label("ADD NEW ITEM", systemImage: "plus")
                               .foregroundColor(.white))
                    }
                    .font(.headline)
                    .frame(height: 64)
                    .frame(maxWidth: .infinity)
                }
            }

        ) {
            Text("Item \(i)")
        }
      }
    }
  }
}

Solution 4:[4]

You can use .listRowInsets for defining last row content insets.

Sample code snippet below:

struct AddButtonInLastSection: View {
    var data = [[1: [1, 2, 3, 4, 5, 6]], [2: [1, 2, 3, 4, 5, 6]], [3: [1, 2, 3, 4, 5, 6]]]

    var body: some View {
        VStack {
            List {
                ForEach(data, id: \.self) { dict in
                    ForEach(0 ..< dict.keys.count) { i in
                        Section(header: Text("Item \(dict.keys[dict.index(dict.startIndex, offsetBy: i)])")
                        ) {
                            ForEach(0 ..< dict.values.count) { j in
                                ForEach(dict.values[dict.index(dict.startIndex, offsetBy: j)], id: \.self) { k in
                                    if dict.keys[dict.index(dict.startIndex, offsetBy: i)] == 3 {
                                        if k == 6{
                                            Text("Items: \(k) last")
                                                .listRowInsets(.init(top: 16, leading: 20, bottom: 64, trailing: 0))
                                        }else {
                                            Text("Items: \(k)")

                                        }
               
                                    } else {
                                        Text("Items: \(k)")
                                    }
                                }
                            }
                        }
                    }
                }
            }
            Button(action: {}) {
                RoundedRectangle(cornerRadius: 4)
                    .overlay(
                        Label("ADD NEW ITEM", systemImage: "plus")
                            .foregroundColor(.white))
            }
            .foregroundColor(Color.red)
            .font(.headline)
            .frame(height: 64)
            .frame(maxWidth: .infinity)
            .padding(.init(top: 0, leading: 8, bottom: 0, trailing: 8))
        }
    }
}

Solution 5:[5]

Overview

  • You could add a toolbar, that way you don't have to worry about content inset.
  • The toolbar would always stay on top and contents would scroll below the toolbar
  • Inset is automatically taken care of

Screenshot:

Toolbar with button

Code:

struct ContentView: View {
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<100) { index in
                    Text("Item \(index)")
                }
            }
            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    Button(action: doSomething) {
                        RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))
                            .foregroundColor(.blue)
                            .overlay(alignment: .center) {
                                Text("Button")
                                    .foregroundColor(.white)
                            }
                            .frame(width: 300, height: 48)
                    }
                }
            }
            .navigationTitle("Title")
        }
    }

    private func doSomething() {

    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Detr01tDave
Solution 3
Solution 4 MobileMatrix
Solution 5 user1046037