Is there a way to put a view in the list header without sections? The table views normally have a property called tableHeaderView which is a header for the whole table and has nothing to do with section headers.
I'm trying to have a list and be able to scroll it like tableHeaderView from UITableView.
struct MyView: View {
let myList: [String] = ["1", "2", "3"]
var body: some View {
VStack {
MyHeader()
List(myList, id: \.self) { element in
Text(element)
}
}
}
}
Thanks to Asperi. This is my final solution for the table header.
struct MyHeader: View {
var body: some View {
Text("Header")
}
}
struct DemoTableHeader: View {
let myList: [String] = ["1", "2", "3"]
var body: some View {
List {
MyHeader()
ForEach(myList, id: \.self) { element in
Text(element)
}
}
}
}
Here is a simple demo of possible approach, everything else (like style, sizes, alignments, backgrounds) is extendable & configurable.
Tested with Xcode 11.4 / iOS 13.4
struct MyHeader: View {
var body: some View {
Text("Header")
.padding()
.frame(maxWidth: .infinity)
}
}
struct DemoTableHeader: View {
let myList: [String] = ["1", "2", "3"]
let headerHeight = CGFloat(24)
var body: some View {
ZStack(alignment: .topLeading) {
MyHeader().zIndex(1) // << header
.frame(height: headerHeight)
List {
Color.clear // << under-header placeholder
.frame(height: headerHeight)
ForEach(myList, id: \.self) { element in
Text(element)
}
}
}
}
}
Related
I'm working on Tabview with page style and I want to scroll tabview on button actions. Buttons are added inside NavigationMenu.
NavigationMenu view and NavigationModel(ViewModel) are separated from a parent.
Selection handling is done inside NavigationModel.
On tab page swipe I'm able to see the change in NavigationMenu which is fine.
But if I tap on buttons the tabview page is not swiping. Even I receive change event on method onReceive.
Code:
import SwiftUI
import Combine
final class NavigationModel: ObservableObject {
#Published var selectedItem = ""
#Published var items: [String] = [
"Button 1", "Button 2", "Button 3"
]
}
struct NavigationMenu: View {
#ObservedObject var viewModel: NavigationModel
var body: some View {
HStack {
ForEach(0..<3, id: \.self) { index in
let title = viewModel.items[index]
Button {
viewModel.selectedItem = title
} label: {
Text(title)
.font(.system(.body))
.padding()
.foregroundColor(
viewModel.selectedItem == title ? .white : .black
)
.background(viewModel.selectedItem == title ? .black : .yellow)
}
}
}
}
}
final class TabViewModel: ObservableObject {
var navModel = NavigationModel()
}
struct TabviewWithMenuView: View {
#ObservedObject var viewModel = TabViewModel()
var body: some View {
parentView
}
private var parentView: some View {
VStack(spacing: 0) {
Spacer()
NavigationMenu(viewModel: viewModel.navModel)
pageView
}
.onReceive(viewModel.navModel.$selectedItem) { output in
print("Button tapped:", output)
}
}
private var pageView: some View {
TabView(selection: $viewModel.navModel.selectedItem) {
ForEach(0..<3, id: \.self) { index in
let tag = viewModel.navModel.items[index]
item(tag: tag)
.tag(tag)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.transition(.slide)
}
private func item(tag: String) -> some View {
VStack {
Text("PAGE: " + tag)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
}
}
Image:
ObservableObject inside ObservableObject is not observed, we need to observe explicitly the instance which is changed.
A possible solution in this case is to separate PageView and inject navigation view model to it so it would be observed.
Tested with Xcode 13.3 / iOS 15.4
Here is main part:
NavigationMenu(viewModel: viewModel.navModel)
PageView(navModel: viewModel.navModel)
...
struct PageView: View {
#ObservedObject var navModel: NavigationModel
var body: some View {
pageView
}
// ....
}
Test module in project is here
I'm trying to embed a view that contains a Header view and a List The header and list render in the original view, but the list completely disappears when I put it in the parent view. I don't understand what I'm doing wrong. I tried putting the header in the list, but then the entire view disappears.
Here is the code for the view that contains the list:
import SwiftUI
struct CityDetailCategoryView: View {
let cityDetailViewModel: CityDetailViewModel
let titles = ["Category 2", "Category 3", "Category 4", "Category 5", "Category 6", "Category 7"]
var body: some View {
VStack(alignment: .leading) {
SectionHeadingView(cityDetailViewModel: CityDetailViewModel, sectionTitle: "Categories")
List {
ForEach(titles, id: \.self) { title in
Text(title)
.font(.title3)
.fontWeight(.medium)
}
}
}
}
}
struct CityDetailCategoryView_Previews: PreviewProvider {
static var previews: some View {
CityDetailCategoryView(cityDetailViewModel: CityDetailViewModel())
.previewLayout(.sizeThatFits)
}
}
...and the code of the parent view:
import SwiftUI
struct CityDetailView: View {
#ObservedObject var cityDetailViewModel: CityDetailViewModel
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(alignment: .leading, spacing: 6, content: {
DetailHeaderImageView(cityDetailViewModel: CityDetailViewModel)
Group {
CityDetailTitleView(cityDetailViewModel: CityDetailViewModel)
CityDetailQuickFactsView(cityDetailViewModel: CityDetailViewModel)
CityDetailCategoryView(cityDetailViewModel: CityDetailViewModel)
CityDetailMiscellaneousView(cityDetailViewModel: CityDetailViewModel)
HStack {
Spacer()
Text("Data provided by ****")
.font(.caption2)
Spacer()
}
.padding()
}
.padding(.horizontal, 24)
})
})
.edgesIgnoringSafeArea(.top)
.navigationBarTitle(cityDetailViewModel.cityDetail.name, displayMode: .inline)
.navigationBarHidden(true)
}
}
}
struct CityDetailView_Previews: PreviewProvider {
static var previews: some View {
CityDetailView(cityDetailViewModel: CityDetailViewModel())
}
}
image of the view containing the list:
image of the parent view:
You're embedding a scrolling view (List) inside another scrolling view (ScrollView), which is always going to have some inherent difficulties (and potentially strange UI/UX consequences with scrolling).
Because the CityDetailCategoryView doesn't have any inherent height besides its header (the rest can be collapsed into the ScrollView), it collapses to zero.
You can use frame to set a height for it, as shown in this simplified example (if you remove the frame call, it behaves the same as yours):
var items = [1,2,3,4,5,6]
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(items, id: \.self) { item in
Text("item")
}
List {
ForEach(items, id: \.self) { item in
Text("item")
}
}.frame(height: 200) //<-- here
Text("last item")
}
}
}
That being said, I think you'll still run the risk of funny scrolling behavior.
My suggestion would be to rearchitect your views so that you have one list or scrolling view for all of the content.
I have a grouped list:
How to change the systemGroupedBackgroundColor of the List?
Here is the code:
struct View2: View {
var array = ["1", "2", "3"]
#State private var selected: String?
var body: some View {
List(array, id:\.self) { value in
Text("\(value) Navigation 1")
.listRowBackground(Color.green)
NavigationLink("", destination: View1())
}.background(Color.red)
.listStyle(GroupedListStyle())
}
}
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
View2()
}
}.navigationBarTitle("Navigation")
}
}
I've tried set the background of the List to Red, but doesn't work. Like below. Thanks!
Insert the following code into View2 and it should work:
init() {
UITableView.appearance().backgroundColor = .red
}
I have a SwiftUI app with a basic List/Detail structure. A new item is created from
a modal sheet. When I create a new item and save it I want THAT list item to be
selected. As it is, if no item is selected before an add, no item is selected after
an add. If an item is selected before an add, that same item is selected after the
add.
I'll include code for the ContentView, but this is really the simplest example of
List/Detail.
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addNewResort = false
#State private var coverDeletedDetail = false
#Environment(\.presentationMode) var presentationMode
var body: some View {
List {
ForEach(resortStore.resorts) { resort in
NavigationLink(destination: ResortView(resort: resort)) {
HStack(spacing: 20) {
Image("FlatheadLake1")
//bunch of modifiers
VStack(alignment: .leading, spacing: 10) {
//the cell contents
}
}
}
}
.onDelete { indexSet in
self.removeItems(at: [indexSet.first!])
self.coverDeletedDetail.toggle()
}
if UIDevice.current.userInterfaceIdiom == .pad {
NavigationLink(destination: WelcomeView(), isActive: self.$coverDeletedDetail) {
Text("")
}
}
}//list
.onAppear(perform: self.selectARow)
.navigationBarTitle("Resorts")
.navigationBarItems(leading:
//buttons
}//body
func removeItems(at offsets: IndexSet) {
resortStore.resorts.remove(atOffsets: offsets)
}
func selectARow() {
//nothing that I have tried works here
print("selectARow")
}
}//struct
And again - the add item modal is extremely basic:
struct AddNewResort: View {
//bunch of properties
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName, country: self.resortCountry, description: self.resortDescription, imageCredit: "Credit", price: Int(self.resortPriceString) ?? 0, size: Int(self.resortSizeString) ?? 0, snowDepth: 20, elevation: 3000, runs: 40, facilities: ["bar", "garage"])
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
To show the issue - The list with a selection:
The list after a new item created showing the previous selection:
Any guidance would be appreciated. Xcode 11.4
I tried to reconstitute your code as closely as could so that it builds. Here is what I have in the end. We have a list of resorts and when a new resort is saved in the AddNewResort sheet, if we are currently in split view (horizontalSizeClass is regular), we will select the new resort, otherwise just dismiss the sheet.
import SwiftUI
class ResortStore: ObservableObject {
#Published var resorts = [Resort(id: UUID(), name: "Resort 1")]
}
struct ContentView: View {
#ObservedObject var resortStore = ResortStore()
#State private var addingNewResort = false
#State var selectedResortId: UUID? = nil
var navigationLink: NavigationLink<EmptyView, ResortView>? {
guard let selectedResortId = selectedResortId,
let selectedResort = resortStore.resorts.first(where: {$0.id == selectedResortId}) else {
return nil
}
return NavigationLink(
destination: ResortView(resort: selectedResort),
tag: selectedResortId,
selection: $selectedResortId
) {
EmptyView()
}
}
var body: some View {
NavigationView {
ZStack {
navigationLink
List {
ForEach(resortStore.resorts, id: \.self.id) { resort in
Button(action: {
self.selectedResortId = resort.id
}) {
Text(resort.name)
}
.listRowBackground(self.selectedResortId == resort.id ? Color.gray : Color(UIColor.systemBackground))
}
}
}
.navigationBarTitle("Resorts")
.navigationBarItems(trailing: Button("Add Resort") {
self.addingNewResort = true
})
.sheet(isPresented: $addingNewResort) {
AddNewResort(selectedResortId: self.$selectedResortId)
.environmentObject(self.resortStore)
}
WelcomeView()
}
}
}
struct ResortView: View {
let resort: Resort
var body: some View {
Text("Resort View for resort name: \(resort.name).")
}
}
struct AddNewResort: View {
//bunch of properties
#Binding var selectedResortId: UUID?
#State var resortName = ""
#Environment(\.presentationMode) var presentationMode
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#EnvironmentObject var resortStore: ResortStore
var body: some View {
VStack {
Text("Add a Resort")
VStack {
TextField("Enter a name", text: $resortName)
//the rest of the fields
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(EdgeInsets(top: 20, leading: 30, bottom: 20, trailing: 30))
Button(action: {
let newResort = Resort(id: UUID(), name: self.resortName)
self.resortStore.resorts.append(newResort)
self.presentationMode.wrappedValue.dismiss()
if self.horizontalSizeClass == .regular {
self.selectedResortId = newResort.id
}
}) {
Text("Save Trip")
}
.padding(.trailing, 20)
}
}
}
struct WelcomeView: View {
var body: some View {
Text("Welcome View")
}
}
struct Resort {
var id: UUID
var name: String
}
We need to keep track of the selectedResortId
We create an invisible NavigationLink that will programmatically navigate to the selected resort
We make our list row a Button, so that the user can select a resort by tapping on the row
I started writing a series of articles about navigation in SwiftUI List view, there are a lot of points to consider while implementing programmatic navigation.
Here is the one that describes this solution that I'm suggesting: SwiftUI Navigation in List View: Programmatic Navigation. This solution works at the moment on iOS 13.4.1. SwiftUI is changing rapidly, so we have to keep on checking.
And here is my previous article that explains why a more simple solution of adding a NavigationLink to each List row has some problems at the moment SwiftUI Navigation in List View: Exploring Available Options
Let me know if you have questions, I'd be happy to help where I can.
I want to make my List inside a ScrollView so that I can scroll List rows and headers together.
But I found that List inside ScrollView isn't working. It shows nothing.
I should use both of them.
I should use ScrollView so that I can make my header(image or text) also scrolled when I scroll the rows.
I should use List to use .ondelete() method.
my sample code is below.
#State private var numbers = [1,2,3,4,5,6,7,8,9]
var body: some View {
ScrollView {
Text("header")
List {
ForEach(numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}
}
}
Anyone know why this happens and(or) how to fix?
It is possible but not when the List is using the full screen.
In the code example I used GeometryReader to make the list as big as possible. But you can also remove the GeometryReader and just insert a fixed dimension into .frame()
struct ContentView: View {
#State private var numbers = [1,2,3,4,5,6,7,8,9]
var body: some View {
GeometryReader { g in
ScrollView {
Text("header")
List {
ForEach(self.numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}.frame(width: g.size.width - 5, height: g.size.height - 50, alignment: .center)
}
}
}
}
There is no need for two scrolling objects. You can also use section for this:
#State private var numbers = [1,2,3,4,5,6,7,8,9]
var body: some View {
List {
Section.init {
Text("Header")
}
ForEach(numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}
}
Just put header inside the List, like
var body: some View {
List {
Text("Header").font(.title)
ForEach(numbers, id: \.self) {
Text("\($0)")
}
.onDelete { index in
// delete item
}
}
}