Search code examples
swiftformsswiftuinavigationview

SwiftUI Form with Picker - NavigationView creating additional back function


I'm adding a side loading view to an app which is used for view and adding certain incidents. The basic layout is intended as follows:

enter image description here

Here is the code for this content:

   var body: some View {
    List {
        Section(header: Text("Existing incidents")) {
            ForEach(incidents) { incident in
                
                VStack(alignment: .leading) {
                    HStack {
                        Image.General.incident
                            .foregroundColor(Constants.iconColor)
                        Text(incident.name)
                            .foregroundColor(Constants.textColor)
                            .bold()
                    }
                    Spacer()
                    HStack {
                        Image.General.clock
                            .foregroundColor(Constants.iconColor)
                        Text(incident.date)
                            .foregroundColor(Constants.textColor)
                    }
                    Spacer()
                    HStack {
                        Image.General.message
                            .foregroundColor(Constants.iconColor)
                        Text(incident.message)
                            .foregroundColor(Constants.textColor)
                    }
                }
            }
        }
    }
    .listStyle(PlainListStyle())
    
    NavigationView {
        Section(header: Text("Add an incident")
                    .bold()
                    .foregroundColor(Constants.textColor)
                    .padding()) {
            Form {
                HStack {
                    Image.General.select
                    Spacer()
                    Picker("Incident selection", selection: $selectedIncident)
                    {
                        ForEach(incidentsList, id: \.self) {
                            Text($0)
                        }
                    }
                }.padding()
                
                HStack {
                    Image.General.write
                    Spacer()
                    TextField("Additional comments", text: $comments)
                }.padding()
                
                Button {
                    
                }
                
            label: {
                Text("Submit")
                    .foregroundColor(Color(UIColor.white))
            }
            .padding(.top, 5)
            .padding(.bottom, 5)
            .padding(.leading, 20)
            .padding(.trailing, 20)
            .background(Color(UIColor.i6.blue))
            .cornerRadius(5)
            }
        }
    }
    .padding(.bottom)
}

}

I intent to obviously split this code out, however I am confused with the way the Form is working here inside the NavigationView. As you can see, I have a back arrow at the top of the part which leads back to a view which contains just the section header. However, I do not want to navigate backwards from the point. What I intend is to have the section header and form on this main page and the NavigationView is just to enable to picker to work, which it does as follows:

enter image description here

But how can remove the initial back arrow and display the section header on the first view?


Solution

  • I have two potential answers. The first is fixing what you were trying to do. It works, but it is hacky and I would only use it at the end of a view hierarchy, if at all. I mocked your code into a minimal, reproducible example:

    struct SplitViewFormPicker: View {
        @State var incidents = [
            Incident(name: "Incident 1", date: "1/1/21", message: "message 1 is here"),
            Incident(name: "Incident 2", date: "1/2/21", message: "message 2 is here"),
            Incident(name: "Incident 3", date: "1/3/21", message: "message 3 is here"),
            Incident(name: "Incident 4", date: "1/4/21", message: "message 4 is here"),
            Incident(name: "Incident 5", date: "1/5/21", message: "message 5 is here"),
            Incident(name: "Incident 6", date: "1/6/21", message: "message 6 is here"),
            Incident(name: "Incident 7", date: "1/7/21", message: "message 7 is here"),
            Incident(name: "Incident 8", date: "1/8/21", message: "message 8 is here"),
            Incident(name: "Incident 9", date: "1/9/21", message: "message 9 is here"),
            Incident(name: "Incident 10", date: "1/10/21", message: "message 10 is here"),
            Incident(name: "Incident 11", date: "1/11/21", message: "message 11 is here"),
        ]
        
        @State var selectedIncident = ""
        @State var comments = ""
        let incidentsList = ["Incident 1", "Incident 2", "Incident 3", "Incident 4"]
        
        var body: some View {
            VStack {
                List {
                    Section(header: Text("Existing incidents")) {
                        ForEach(incidents) { incident in
                            
                            VStack(alignment: .leading) {
                                HStack {
                                    Image(systemName: "exclamationmark.triangle.fill")
                                        .foregroundColor(Constants.iconColor)
                                    Text(incident.name)
                                        .foregroundColor(Constants.textColor)
                                        .bold()
                                }
                                Spacer()
                                HStack {
                                    Image(systemName: "clock")
                                        .foregroundColor(Constants.iconColor)
                                    Text(incident.date)
                                        .foregroundColor(Constants.textColor)
                                }
                                Spacer()
                                HStack {
                                    Image(systemName: "message.fill")
                                        .foregroundColor(Constants.iconColor)
                                    Text(incident.message)
                                        .foregroundColor(Constants.textColor)
                                }
                            }
                        }
                    }
                }
                .listStyle(PlainListStyle())
                
                NavigationView {
                    Form {
                        Section(header: Text("Add an incident")
                                    .bold()
                                    .foregroundColor(Constants.textColor)
                                    .padding()) {
                            HStack {
                                Image(systemName: "hand.draw")
                                Spacer()
                                Picker("Incident selection", selection: $selectedIncident)
                                {
                                    ForEach(incidentsList, id: \.self) {
                                        Text($0)
                                    }
                                }
                            }.padding()
                            
                            HStack {
                                Image(systemName: "rectangle.and.pencil.and.ellipsis")
                                Spacer()
                                TextField("Additional comments", text: $comments)
                            }.padding()
                            
                            Button {
                                
                            }
                            
                        label: {
                            Text("Submit")
                                .foregroundColor(Color(UIColor.white))
                        }
                        .padding(.top, 5)
                        .padding(.bottom, 5)
                        .padding(.leading, 20)
                        .padding(.trailing, 20)
                        .background(Color(UIColor.systemGray6))
                        .cornerRadius(5)
                        }
                                    .navigationBarHidden(true)
                    }
                }
                .padding(.bottom)
            }
        }
    }
    
    struct Constants {
        static var textColor = Color.black
        static let iconColor = Color.orange
    }
    
    struct Incident: Identifiable {
        let id = UUID()
        let name: String
        let date: String
        let message: String
    }
    

    The second, is flexible, reusable and not hacky in the least:

    struct PartialSheet<Content: View>: View {
        
        var sheetSize: SheetSize
        let content: () -> Content
        
        init(_ sheetSize: SheetSize, @ViewBuilder content: @escaping () -> Content) {
            self.sheetSize = sheetSize
            self.content = content
        }
        
        var body: some View {
            content()
                .frame(height: sheetHeight())
                .animation(.spring(), value: sheetSize)
        }
        
        private func sheetHeight() -> CGFloat {
            switch sheetSize {
            case .quarter:
                return UIScreen.main.bounds.height / 4
            case .half:
                return UIScreen.main.bounds.height / 2
            case .threeQuarter:
                return UIScreen.main.bounds.height * 3 / 4
            case .full:
                return .infinity
            }
        }
    }
    
    enum SheetSize: Equatable {
        case quarter, half, threeQuarter, full
    }
    

    Use it like this in SplitViewFormPicker:

        NavigationView {
            VStack {
                List {
                // The same code as above not repeated to save space
                }
                .listStyle(PlainListStyle())
            
                // Call PartialSheet, tell it what size you want, and pass the view as a closure.
                // It will return that view sized to the screen.
                PartialSheet(.half) {
                    IncidentForm()
                }
            }
        }
    }
    

    You may notice that because the views are in a VStack, that the biggest the IncidentForm will get is half of the screen. However, if you put it in a ZStack it will limit itself as you requested. Use it how you need.