11
votes

How do I top align a Form within a NavigationView. I have the below, when I remove NaviagationView from the body, the Form is top aligned. With NavigationView in place, I get spacing in between(red box added to show space). I can use .padding(.top, -20) on the Form but while this works it skews things slightly.

Screenshot

NavigationView {
    Form {
        VStack {
            HStack {
                Text("Name:").underline().font(.headline)
                TextField("Name", text: $routine.name)
                
            }
            roundPicker()
            TimeSelectionView(stateName: "A", stateDuration: "0:30", stateBackground: "#df405a")
            TimeSelectionView(stateName: "B", stateDuration: "1:30", stateBackground: "#4ea1d3")
            TimeSelectionView(stateName: "C", stateDuration: "3:00", stateBackground: "#4f953b")
        }
    }
    .navigationBarTitle("Create", displayMode: .inline)
    .navigationBarItems(trailing:
        Button(action: {
            //Save Routine
            self.routine.rounds = self.roundsArray[self.rounds]
            
            print("Workout Name: \(self.routine.name)")
            print("Workout Rounds: \(self.routine.rounds)")
            }, label: {
                Text("Save")
            }
        )
    )
} 
4
The gap also appears when applying .listStyle(GroupedListStyle()) to a List view and the accepted answer also works in that case.Teo Sartori

4 Answers

27
votes

SwiftUI Form is actually a grouped style UITableView under the hood and that is default tableHeaderView. So you can remove it like this:

iOS 13

struct ContentView: View {
    
    init() { // this can be done in `onAppear` modifier if you need to restore the appereance later on `onDisappear`
        UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
    }
    
    var body: some View {
        ,,,
    }
}

iOS 14

Apple is limiting the appearance hack and setting the tableHeaderView's frame is one of them so we need to dig more around by adding these steps:

1. Use constraint instead of frame:

init() {
    let view = UIView()
    view.translatesAutoresizingMaskIntoConstraints = false
    view.heightAnchor.constraint(equalToConstant: 0).isActive = true
    UITableView.appearance().tableHeaderView = view
}

2. Force reload the table:

The constraint is added but it needs a reload. SwiftUI does not allow you to reload a list manually. So we need to add a dummy view and a dummy state to trick it:

@State var loaded = false // need for stay update
,,,

    List {

        if loaded {
            Text("Actual content of the list")

        } else {
            Text("Dummy content. Not important")
                .onAppear {
                    loaded = true // need for instatnt force update 
                }
        }

    }

Note1:

Try never hard-code any numbers (like -35 for inset!). Numbers vary on different devices and platforms and states and etc.

Note2:

.zero frame is not working. You should pass at least a minimum height if you use frame for iOS 13 or UIKit.

Note3:

All methods are hacks and Apple is trying to restrict access to underlying types like UITableView. So stay up to date and try to help the community.

Note4:

Contribute to the community by upvoting and commenting. So people will be more motivated to dig in unknown places and bring us cool stuff.

Note5:

Since we are using the Appearance proxy, any change will be applied to all TableViews in the app. You can store the state of the original Appearance -> apply the new style onAppear and restore the original onDisaper if needed.

12
votes

Setting the table header view is no longer working for me in Xcode 12. Instead, setting a negative top content inset seems to achieve the same thing.

UITableView.appearance().contentInset.top = -35
1
votes

if you use Introspect add this to your NavigationView

.introspectTableView {
    $0.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
-3
votes

Cleaner add a navbartitle to form section and then set hidden

Form {
// add end of form section
}.navigationBarHidden(true).navigationBarTitle("Title") // Form