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.
My current solution is the following
Section(header: Color.clear.frame(height: 64)) { EmptyView() }
but I wonder is there a better way?
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)
}
}
}
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
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
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)")
}
}
}
}
}
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))
}
}
}
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:
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() {
}
}
Try using the below code
.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 64, right: 0)
Related
I'm creating a dinner menu with various food item's that can be tapped. Each item is wrapped in a NavigationLink that leads to it's detail page.
How can the item's name be placed as the navigation bar title for each item? Here's the entire MenuItemsView struct and a gif to demonstrate where I'd like the food item title just as in the previous Menu screen.
struct MenuItemsView: View {
let food = (1...12).map { "Food \($0)" }
let drinks = (1...8).map { "Drink \($0)" }
let dessert = (1...4).map { "Dessert \($0)" }
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
NavigationView {
ScrollView {
VStack {
Text("Food")
.frame(maxWidth: .infinity, alignment: .leadingFirstTextBaseline)
.font(.title)
.padding(.init(top: -5, leading: 16, bottom: 0, trailing: 0))
LazyVGrid(columns: columns, spacing: 5.0) {
ForEach(food, id: \.self) { item in
NavigationLink(destination: MenuItemDetailsView()) {
VStack {
ColorSquare(color: .black)
Text(item)
}
.padding(.init(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
}
VStack {
Text("Drinks")
.frame(maxWidth: .infinity, alignment: .leadingFirstTextBaseline)
.font(.title)
.padding(.init(top: -5, leading: 16, bottom: 0, trailing: 0))
LazyVGrid(columns: columns, spacing: 5.0) {
ForEach(drinks, id: \.self) { item in
VStack {
ColorSquare(color: .black)
Text(item)
}
.padding(.init(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
VStack {
Text("Desert")
.frame(maxWidth: .infinity, alignment: .leadingFirstTextBaseline)
.font(.title)
.padding(.init(top: -5, leading: 16, bottom: 0, trailing: 0))
LazyVGrid(columns: columns, spacing: 5.0) {
ForEach(dessert, id: \.self) { item in
VStack {
ColorSquare(color: .black)
Text(item)
}
.padding(.init(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
}
.navigationBarTitle("Menu")
.navigationBarItems(trailing:
Button(action: {
print("Edit button pressed...")
}) {
NavigationLink(destination: MenuItemsOptionView()) {
Image(systemName: "slider.horizontal.3")
}
}
)
}
}
}
Food Details Demonstration
As a bonus, if anyone can tell me how to properly line up the Color Squares with their respective category name and Menu title, I'd appreciate it a lot lol. Thanks!
#State itemname:String
var body: some View {
...
.navigationTitle(){
Text(itemname)
}
I was able to figure out this issue. Here's how:
I passed in itemName: item as a NavigationLink parameter as the destination. In the MenuItemDetailsView file, the body was set up with a Text view and navigationBarTitle modifier with itemName passed in. itemName was created above the MenuItemDetailsView struct before being passed of course. Here is the MenuItemDetailsView and MenuItemsView code that solved the problem as well as a quick demonstration:
MenuItemDetailsView
import SwiftUI
struct MenuItemDetailsView: View {
var itemName: String
var body: some View {
Text(itemName)
.navigationBarTitle(itemName)
}
}
struct MenuItemDetailsView_Previews: PreviewProvider {
static var previews: some View {
let food = (1...12).map { "Food \($0)" }
return Group {
ForEach(food, id: \.self) { item in
NavigationView {
MenuItemDetailsView(itemName: item)
}
.previewDisplayName(item)
}
}
}
}
MenuItemsView
import SwiftUI
struct ColorSquare: View {
let color: Color
var body: some View {
color
.frame(width: 100, height: 100)
}
}
struct MenuItemsView: View {
let food = (1...12).map { "Food \($0)" }
let drinks = (1...8).map { "Drink \($0)" }
let dessert = (1...4).map { "Dessert \($0)" }
let columns = [
GridItem(.adaptive(minimum: 80))
]
var body: some View {
NavigationView {
ScrollView {
VStack {
Text("Food")
.frame(maxWidth: .infinity, alignment: .leadingFirstTextBaseline)
.font(.title)
.padding(.init(top: -5, leading: 16, bottom: 0, trailing: 0))
LazyVGrid(columns: columns, spacing: 5.0) {
ForEach(food, id: \.self) { item in
NavigationLink(destination: MenuItemDetailsView(itemName: item)) {
VStack {
ColorSquare(color: .black)
Text(item)
}
.padding(.init(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
}
}...more code
Food Item Nav Title Solved Demo
I'm busy with a screen in my app that is driving me nuts. I could use a good advice and code guidance. Here two screen dumps of the screen in action. The photo is fixed and stays put. The text below the photo, the picker and the rows can scroll.
I want my user to tap on the red lines to edit the values. I thought that I could easily make a List out of the rows. But as soon as I add a Line {} around the code the rows will not display anymore.
struct WeightListView: View {
#Binding var descendingDays: Bool
var egg: Egg
var totalWeights : [ActualWeight] {
var totalWeights = egg.actualWeights
if descendingDays { totalWeights = totalWeights.sorted { $0.day > $1.day } }
else { totalWeights = totalWeights.sorted { $0.day < $1.day } }
return totalWeights
}
var body: some View {
let count = totalWeights.reduce(0) { $1.measured ? $0 + 1 : $0 }
VStack {
HStack(alignment: .lastTextBaseline) {
Text("Egg weights")
.font(.title2)
.padding(.top, 40)
Spacer()
Text("\(count) measurements")
.font(.callout)
.foregroundColor(.secondary)
}
.padding(EdgeInsets(top: -25, leading: 0, bottom: 0, trailing: 0))
}
ForEach(totalWeights, id: \.self) { totalWeight in
VStack {
HStack(alignment: .center) {
if totalWeight.measured {
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(.green)
} else {
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(.red)
}
Spacer()
Text("17 Apr")
Spacer()
Text("\(formatVar2(getal: totalWeight.weight)) gr")
Spacer()
Text("\(formatVar1(getal: totalWeight.prediction)) %")
.multilineTextAlignment(.trailing)
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.padding(EdgeInsets(top: -5, leading: 0, bottom: 0, trailing: 0))
}
}
}
}
I tried to edit the code with a NavigationView and a List, like this:
NavigationView {
List {
ForEach(totalWeights, id: \.self) { totalWeight in
NavigationLink(destination: WeightDetailView()) {
VStack {
HStack(alignment: .center) {
if totalWeight.measured {
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(.green)
} else {
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(.red)
}
Spacer()
Text("17 Apr")
Spacer()
Text("\(formatVar2(getal: totalWeight.weight)) gr")
Spacer()
Text("\(formatVar1(getal: totalWeight.prediction)) %")
.multilineTextAlignment(.trailing)
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.padding(EdgeInsets(top: -5, leading: 0, bottom: 0, trailing: 0))
}
}
The endresult is that all the rows are blanco and thus gone. I don't get why, can't you combine a List with other components in a View?
You need to wrap everything in another VStack.
In this case the Navigation View does not add to the problem but it should be always your top layer.
NavigationView{
VStack{
VStack{
// Your image and headline stuff
}
List{
// ForEach ...
}
}
}
There is another thing I'd like to mention. It doesn't has to do with your problem but you can save a lot of double code by replacing
this:
if totalWeight.measured {
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(.green)
} else {
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(.red)
}
by this:
Text("Day: \(totalWeight.day)")
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 10))
.foregroundColor(totalWeight.measured ? .green : .red)
I am currently having trouble with my Custom Tab Bar there is a gray area above it (Tab View) that controls each tab but I need that to go under my custom tab bar but functionality of the TabView still be in effect and be used with the icons. You can hide the Tab bar with UITabBar.apperance() which gets rid of the gray area but no longer has any functions.. but I need that gray area to go under the tabs. If that makes sense?
Home.swift
import SwiftUI
struct Home: View {
//Hiding Tab Bar..
init() {
UITabBar.appearance().isHidden = false
}
var body: some View {
VStack(spacing: 0){
//Tab View...
TabView{
Color.blue
.tag("house.circle")
Color.green
.tag("pencil")
Color.pink
.tag("magnifyingglass")
Color.red
.tag("bell")
Color.yellow
.tag("cart")
}
//Custom Tab Bar...
CustomTabBar()
}
.ignoresSafeArea()
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
//Extending View To Get Screen Frame...
extension View {
func getRect()->CGRect {
return UIScreen.main.bounds
}
}
CustomTabBar.swift
import SwiftUI
struct CustomTabBar: View {
var body: some View {
HStack(spacing: 0){
// Tab Bar Button...
TabBarButton(systemName: "house.circle")
.background(Color.blue)
TabBarButton(systemName: "pencil")
.background(Color.green)
Button(action: {}, label: {
Image(systemName: "magnifyingglass")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width:24, height:24)
.foregroundColor(.white)
.padding(20)
.background(Color.green)
.clipShape(Circle())
//Shadows
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 5, y: 5)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: -5, y: -5)
})
.tag("magnifyingglass")
TabBarButton(systemName: "bell")
.background(Color.red)
TabBarButton(systemName: "cart")
.background(Color.yellow)
}
.padding(.top)
//Decreasing the extra padding added...
.padding(.vertical, -0)
.padding(.bottom,getSafeArea().bottom == 0 ? 15 : getSafeArea().bottom)
.background(Color.white)
}
}
struct CustomTabBar_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView()
}
}
}
//extending view to get safe area...
extension View {
func getSafeArea()-> UIEdgeInsets {
return UIApplication.shared.windows.first?.safeAreaInsets ?? UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}
struct TabBarButton: View {
var systemName: String
var body: some View{
Button(action: {
}, label: {
VStack(spacing: 8){
Image(systemName)
.resizable()
//Since its asset image...
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width:28, height: 28)
}
.frame(maxWidth: .infinity)
})
}
}
EDIT: SECOND IMAGE I am hiding the tab bar setting it to true instead of false.
//Hiding Tab Bar..
init() {
UITabBar.appearance().isHidden = true
}
you could try this to "cover" the original TabView bar:
In Home replace VStack with ZStack.
and
struct CustomTabBar: View {
var body: some View {
VStack (alignment: .leading) {
Spacer()
HStack(spacing: 0) {
TabBarButton(systemName: "house.circle").background(Color.blue)
TabBarButton(systemName: "pencil").background(Color.green)
Button(action: {}, label: {
Image(systemName: "magnifyingglass")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width:24, height:24)
.foregroundColor(.white)
.padding(20)
.background(Color.green)
.clipShape(Circle())
//Shadows
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 5, y: 5)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: -5, y: -5)
})
.tag("magnifyingglass")
TabBarButton(systemName: "bell").background(Color.red)
TabBarButton(systemName: "cart").background(Color.yellow)
}
}
.padding(.bottom, getSafeArea().bottom == 0 ? 15 : getSafeArea().bottom)
.background(Color.white)
}
}
you will then need to implement the action of each of your CustomTabBar buttons.
EDIT1:
ok, as I mentioned you need to implement the actions for your buttons.
There are many ways to do this, this is just one approach:
struct CustomTabBar: View {
#Binding var tagSelect: String
var body: some View {
VStack (alignment: .leading) {
Spacer()
HStack(spacing: 0) {
TabBarButton(tagSelect: $tagSelect, systemName: "house.circle").background(Color.blue)
TabBarButton(tagSelect: $tagSelect, systemName: "pencil").background(Color.green)
Button(action: {}, label: {
Image(systemName: "magnifyingglass")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width:24, height:24)
.foregroundColor(.white)
.padding(20)
.background(Color.green)
.clipShape(Circle())
//Shadows
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 5, y: 5)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: -5, y: -5)
})
.tag("magnifyingglass")
TabBarButton(tagSelect: $tagSelect, systemName: "bell").background(Color.red)
TabBarButton(tagSelect: $tagSelect, systemName: "cart").background(Color.yellow)
}
}
.padding(.bottom,getSafeArea().bottom == 0 ? 15 : getSafeArea().bottom)
// no background or use opacity, like this
.background(Color.white.opacity(0.01)) // <-- important
}
}
extension View {
func getSafeArea()-> UIEdgeInsets {
return UIApplication.shared.windows.first?.safeAreaInsets ?? UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}
struct TabBarButton: View {
#Binding var tagSelect: String
var systemName: String
var body: some View{
Button(action: {tagSelect = systemName }, label: {
VStack(spacing: 8){
Image(systemName)
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width:28, height: 28)
}
.frame(maxWidth: .infinity)
})
}
}
struct Home: View {
#State var tagSelect = "house.circle"
init() {
UITabBar.appearance().isHidden = false
}
var body: some View {
ZStack {
TabView (selection: $tagSelect) {
Color.blue.tag("house.circle")
Color.green.tag("pencil")
Color.pink.tag("magnifyingglass")
Color.red.tag("bell")
Color.yellow.tag("cart")
}
CustomTabBar(tagSelect: $tagSelect)
}
.ignoresSafeArea()
}
}
extension View {
func getRect()->CGRect {
return UIScreen.main.bounds
}
}
I have created a dashboard with 9 buttons laid out on a lazyVGrid and a ForEach loop to display the various buttons.
(Click to expand)
Now, I want to navigate to various new screens based on the button pressed. Someone, please help me achieve this.
LazyVGrid(columns: row, spacing: 25) {
ForEach(Dashboard_Data) { data in
Button(action: {
// action
}) {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
VStack(alignment: .leading, spacing: 5) {
Spacer(minLength: 0)
Text(data.data)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
HStack{
Spacer(minLength: 0)
Text(data.suggest)
.font(.system(size: 17, weight: .regular))
.lineLimit(2)
.foregroundColor(.white)
}
}.padding()
.background(Color(data.image))
.cornerRadius(20)
.shadow(color: Color.black.opacity(0.5), radius: 5, x: 0, y: 5)
Image(data.imageIcon)
.renderingMode(.template)
.foregroundColor(.white)
.font(.system(size: 25, weight: .regular))
.padding(10)
.background(Color.white.opacity(0.15))
.clipShape(Circle())
}
}
}
}
var body: some View{
LazyVGrid(columns: row, spacing: 25) {
ForEach(Dashboard_Data) { data in
NavigationLink(destination: getTheDestination(data)) {
//label/design of your button
}
}
}
}
private func getTheDestination(data:<put ur dataType of Dashboard_Data> ) -> some View {
// add some input parameter in this func to get data/informattion from your Dashboard_Data
// according to ur data return the destination view
if data.data == "your value" {
return Text( _ data.data)
}
return Text("Destination")
}
I made a card style List row which work fine. The issue is that the list row background color remains. I can make it disappear by setting it to systemGray6 but it's not very adaptive for dark mode and there is very likely a better way of doing this.
Card View:
var body: some View {
ZStack {
Rectangle().fill(Color.white)
.cornerRadius(10).shadow(color: .gray, radius: 4)
.frame(width: UIScreen.main.bounds.width - 40)
HStack {
Image(uiImage: (UIImage(data: myVideo.thumbnailImage!) ?? UIImage(systemName: "photo"))!)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 100)
VStack {
Text(myVideo.title!)
.multilineTextAlignment(.center)
.font(.body)
.padding(.bottom, 10)
.frame(maxWidth: UIScreen.main.bounds.width - 40, maxHeight: .infinity, alignment: .center)
Text(myVideo.youtuber!)
.font(.subheadline)
}
}
.padding(5)
}
}
List View:
var body: some View {
NavigationView {
if myVideos.isEmpty {
Text("You have not added videos yet!")
font(.subheadline)
} else {
List() {
Section(header: Text("Not Watched")) {
ForEach(myVideos) { video in
if !video.watched {
NavigationLink(destination: MyVideoView(myVideo: video)) {
MyListRowView(myVideo: video)
}
}
}
.onDelete(perform: removeItems)
}
Section(header: Text("Watched")) {
ForEach(myVideos) { video in
if video.watched {
NavigationLink(destination: MyVideoView(myVideo: video)) {
MyListRowView(myVideo: video)
}
}
}
.onDelete(perform: removeItems)
}
.listRowBackground(Color(UIColor.systemGray6))
}
.navigationBarTitle("Videos")
.listStyle(GroupedListStyle())
}
}
}
Image: (Intended behaviour in "WATCHED" and Incorrect behaviour in "NOT WATCHED"