What does List var in NavigationLink mean in SwiftUI - swift

The following is my code for my simple contentView
struct ContentView: View {
#State private var selection = 1;
#State private var addFood = false;
var listItems = [
Food(name: "List Item One"),
Food(name: "List Item Two")
]
var body: some View {
TabView(selection: $selection) {
NavigationView {
List(listItems){
food in NavigationLink(destination: FoodView(selec: selection)) {
Text(food.name)
}
}.navigationBarTitle(Text("Fridge Items"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: FoodView(selec: selection)) {
Image(systemName: "plus.circle").resizable().frame(width: 22, height: 22)
} )
}
.tabItem {
Image(systemName: "house.fill")
Text("Home")
}
.tag(1)
Text("random tab")
.font(.system(size: 30, weight: .bold, design: .rounded))
.tabItem {
Image(systemName: "bookmark.circle.fill")
Text("profile")
}
.tag(0)
}
}
}
struct FoodView: View{
var selec: Int?
var body: some View{
NavigationView{
Text("food destination view \(selec!)");
}
}
}
I follow some tutorials to get to this point. However, I'm confused about the syntax
List(listItems){
food in NavigationLink(destination: FoodView(selec: selection)) {
Text(food.name)
}
What does the above line mean? My guess is that we list out the items we have, and then for each food we have, we add a navigation link for them. However, that's my guess and I want to know the true syntax behind this. I've already read the documentation about List and also go through the official documentation about swift syntax. I didn't find anything useful. I thought it was a closure first, but I found there is still a difference.
Could anyone help, please.

List is a struct which is a View, and it takes a closure as a parameter when initialising itself.
You can image this closure as a function , so that this function is kind of like this.
func imagineFunc(item: YourItem) -> YourRowContent {
//some code goes here
return YourRowContent
}
Note: above function is only for explanation. there are no such types.
so , when ListView wants to create one of its rows , then List call this given closure(imagineFunc) and get made a RowContent/row view.
According to your code when List view call the given closure(for each row) you have return a NavigationLink(You can imagine this as a button which can navigate to another view when it is inside navigation view). So that each row becomes a NavigationLink in your List.

Related

#AppStorage property wrapper prevents from dismissing views

I have an app with four (4) views, on the first view I'm showing a list of cars pulled from CoreData, the second view is presented when a car is tapped and it shows the services for each car. The third view is presented when tapping on a service, and it shows the details of the selected service. The fourth view is presented when tapping a button and it shows records for the specified service.
The issue I'm having is that for some reason if I use an #AppStorage property wrapper within the ServicesView I cannot dismiss the fourth view (RecordsView). I don't think the issue is with CoreData but let me know if you need to see the code for Core Data.
Any idea why adding an #AppStorage property wrapper in the ServicesView would affect other views?
CarsView
struct CarsView: View {
#ObservedObject var carViewModel:CarViewModel
#State private var carInfoIsPresented = false
var body: some View {
NavigationView{
VStack{
List {
ForEach(carViewModel.cars) { car in
HStack{
VStack(alignment:.leading){
Text(car.model ?? "")
.font(.title2)
Text(car.make ?? "")
.foregroundColor(Color(UIColor.systemGray))
}
NavigationLink(destination: ServicesView(carViewModel: carViewModel, selectedCar: car)){
Spacer()
Text("Services")
.frame(width: 55)
.font(.caption)
.foregroundColor(Color.systemGray)
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Cars")
.accentColor(.white)
.padding(.top, 20)
}
}
}
}
ServicesView
struct ServicesView: View {
#ObservedObject var carViewModel: CarViewModel
var selectedCar: Car
// ISSUE: No issues dismissing the RecordsView if I comment this out
#AppStorage("sortByNameKey") private var sortByName = true
#State private var selectedService: CarService?
var body: some View {
VStack{
List {
ForEach(carViewModel.carServices) { service in
HStack{
Text(service.name ?? "")
.font(.title3)
NavigationLink(destination: ServiceInfoView(carViewModel: carViewModel, selectedCar: selectedCar, selectedService: service)){
Spacer()
Text("Details")
.font(.caption)
.foregroundColor(Color.systemGray)
}
}
}
}
.navigationBarTitle(Text("\(selectedCar.model ?? "Services") - Services"))
.listStyle(GroupedListStyle())
}
.onAppear{
carViewModel.getServices(forCar: selectedCar)
}
}
}
ServiceInfoView
struct ServiceInfoView: View {
#ObservedObject var carViewModel: CarViewModel
#State private var recordsViewIsPresented = false
#State var selectedCar: Car
#State var selectedService: CarService
var body: some View {
VStack{
Text(selectedService.name ?? "")
.font(.largeTitle)
.padding(.bottom)
VStack{
Button(action: openRecordsView) {
Text("Service History")
}
.padding(10)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(15)
}
}
.sheet(isPresented: $recordsViewIsPresented){
RecordsView(carViewModel: carViewModel, selectedService: selectedService)
}
}
func openRecordsView(){
recordsViewIsPresented.toggle()
}
}
RecordsView
struct RecordsView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedService: CarService
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack{
List {
Section(header: Text("Records")) {
ForEach(carViewModel.serviceRecords) { record in
HStack{
Text("Service Date:")
Text("\(record.serviceDate ?? Date(), style: .date)")
.foregroundColor(Color(UIColor.systemGray))
}
}
}
}
.background(Color.purple)
.listStyle(GroupedListStyle())
}
.navigationBarTitle("Records for \(selectedService.name ?? "")", displayMode: .inline)
.navigationBarItems(leading: Button("Cancel", action: dismissView))
.onAppear{
carViewModel.getRecords(forService: selectedService)
}
}
}
func dismissView(){
presentationMode.wrappedValue.dismiss()
}
}
NavigationView can only push one detail screen unless you set .isDetailLink(false) on the NavigationLink.
FYI we don't use view model objects in SwiftUI, you have to learn to use the View struct correctly along with #State, #Binding, #FetchRequest etc. that make the safe and efficient struct behave like an object. If you ignore this and use an object you'll experience the bugs that Swift with its value types was designed to prevent. For more info see this answer MVVM has no place in SwiftUI.

NavigationLink inside .searchable does not work

I understand its new, but this seems like pretty basic functionality that is not here. When implementing a .searchable in the new iOS 15, it would seem that a NavigationLink does not work, at all.
Ideally, the searchable would produce a filtered list with a ForEach and for each item, a Nav Link could take you to another view based on your selection. The ForEach and list works and looks beautiful, it just wont take you anywhere.
Watching WWDC21, they talk an awful lot about .searchable, but very little demonstration/example is given..
Here is a simple example, with no ForEach loop, that shows it does not work at all.. Am I missing something?
Any insight appreciated:
import SwiftUI
struct ContentView: View {
#State private var term = ""
var body: some View {
NavigationView {
Text("Hello, world!")
.padding()
}
.searchable(text: $term) {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View {
Text("Goodbye!")
.padding()
}
}
NavigationLink must always be inside NavigationView, no matter what. If you want to put it outside, like inside .searchable, you should use programmatic navigation with isActive.
struct ContentView: View {
#State private var term = ""
#State private var isPresenting = false
var body: some View {
NavigationView {
/// NavigationView must only contain 1 view
VStack {
Text("Hello, world!")
.padding()
/// invisible NavigationLink
NavigationLink(destination: SecondView(), isActive: $isPresenting) { EmptyView()}
}
}
.searchable(text: $term) {
Button { isPresenting = true } label: {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View {
Text("Goodbye!")
.padding()
}
}
Result:

When pressing button and toggling a Bool var all of my Bool vars toggle?

I have a form with several Boolean variables that change images on toggle, but when I click any of the buttons, all my variables change images? I have pulled all of them into separate subviews with the same result. Doesn't make any sense to me. Any help would be appreciated. Probably something simple I'm overlooking, but I'll be damned if I can see it.
Here is the code.
#State var total: String = ""
#State var date: Date = Date()
#State var bathroom: Bool = false
#State var steps: Bool = false
#State var furniture: Bool = false
#State var travel: Bool = false
#State var woodFloor: Bool = false
#State var concreteFloor: Bool = false
#State var takeUp: Bool = false
var body: some View {
VStack {
Form {
Section(header: Text("Job Info")
.font(.title2)
.foregroundColor(.blue)
.fontWeight(.bold)
.padding(.bottom, 10)
) {
VStack(alignment: .leading) {
DatePicker("Date", selection: $date)
.font(.title3)
.foregroundColor(.black)
.padding(.bottom, 10)
HStack(alignment: .center) {
Text("Total: $")
.font(.title3)
.foregroundColor(.black)
.padding(.trailing, 0)
TextField("", text: $total)
.font(.title3)
.frame(width: 100, height: 20, alignment: .leading)
.background(Color.yellow)
.foregroundColor(.black)
.cornerRadius(5)
Spacer()
}
.padding(.bottom, 20)
}
Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) {
Spacer()
HStack {
Text("Bathroom")
Spacer()
Button {
self.bathroom.toggle()
} label: {
Image(systemName: bathroom ? "checkmark.square" : "square")
}
}
HStack {
Text("Steps")
Spacer()
Button {
self.steps.toggle()
} label: {
Image(systemName: steps ? "checkmark.square" : "square")
}
}
HStack {
Text("Furniture")
Spacer()
Button {
self.furniture.toggle()
} label: {
Image(systemName: furniture ? "checkmark.square" : "square")
}
}
Spacer()
}
}
}
}
You put all buttons in one row (VStack with HStacks creates one view, so one row), and Form (being a List) sends all actions whenever any button is clicked in a row (it is designed to have one active element in a row).
So the solution would be either to remove VStack
Section(header: Text("Addons")
.font(.title3)
.foregroundColor(.blue)
.padding(.bottom, 10)
) {
VStack(alignment: .leading, spacing: 5) { // << this !!
Spacer()
and let every HStack with button live in own row...
... or instead of buttons use Image with tap gesture, like
HStack {
Text("Steps")
Spacer()
Image(systemName: steps ? "checkmark.square" : "square")
.padding()
.onTapGesture {
self.steps.toggle()
}
}
If you have a button inside a Section of Form the whole section will act as a button. So when you tap on it, all 3 button actions get executed.
instead, You can use Image with .onTapGesture{} modifier. In that way, you'll get what you want.
Sample code,
Image(systemName: bathroom ? "checkmark.square" : "square")
.foregroundColor(.blue)
.onTapGesture {
self.bathroom.toggle()
}
I think you are expecting the wrong thing from Form. Forms are not to make flexible lists with fancy stuff. If you want to be able to do anything you want and be able to customize your list at will, you should probably use Grids and other normal components like VStack...
How to use Form and what to expect:
The easiest way to see what you can do or not do with Forms without working around the limitations, is to go to the settings on your iPhone and see how different part of the settings are made. You can achieve almost all those stuff very easily without much work. However, if you want something even a little bit different than what you see in the settings, then you probably need a workaround to implement that in your app because Forms are not flexible.
Example of what you can do with Forms, taken from the settings:
For clarification, i am not saying you can't achieve what you want using forms. I'm saying thats most likely not a good idea to try to force forms to be something that they are not supposed to be.
The Answer:
So what should you do now? You should probably replace your button with Toggles so you get something similar to what you see in the settings.
[Other people have already said how to fix your current problem, so i'll just say the better way]
Consider using something like this:
struct ContentView: View {
#State var date = Date()
#State var total = ""
#State var bathroom = false
#State var steps = false
#State var furniture = false
var body: some View {
NavigationView { // DELETE this if the view before this view
// already has a navigation view
Form {
Section(header: Text("Job Info")) {
DatePicker("Date", selection: $date)
NavigationLink.init(
destination: FormTextFieldView(name: "Total $", value: $total),
label: {
Text("Total")
Spacer()
Text("$ " + total).foregroundColor(.secondary)
})
}
Section(header: Text("Addons")) {
Toggle("Bathroom", isOn: $bathroom)
Toggle("Steps", isOn: $steps)
Toggle("Furniture", isOn: $furniture)
}
}
.navigationBarTitle("Form", displayMode: .inline)
}
}
}
struct FormTextFieldView: View {
let name: String
#Binding var value: String
var body: some View {
Form {
TextField(name, text: $value)
}
.navigationBarTitle(name)
}
}
Why to use this?
First, because it is built with No Effort. You barely need to use any modifiers. So simple!
Second, this will work well on Any apple device. When you use modifiers, specially setting a frame for the view, you'll need to consider what will happen if you use e.g. an iPad. You don't need to worry about that when you are using this approach.

Strange UI rendering problem on iOS 14 when a button is placed on a section header/footer

In the following sample code, a button is placed on a (form-) section header, which will toggle a sheet whenever it is pressed. The sheet has a list of elements to show.
import SwiftUI
struct ContentView: View {
var body: some View {
VStack{
Form{
Section(header: headerView()) {
Text("Some Text")
}
}
}
}
}
struct headerView: View {
#State var showSheet = false
var body: some View {
Button(action: { self.showSheet.toggle()}){
HStack{
Spacer()
Image(systemName: "pencil.and.ellipsis.rectangle")
Text("View Sheet")
}
}.sheet(isPresented: $showSheet) {sheetView()}
}
}
struct sheetView: View {
#Environment(\.presentationMode) private var presentationMode
var body: some View {
NavigationView{
VStack(alignment: .leading) {
List() {
Text("List element 1")
Text("List element 2")
Text("List element 3")
Text("List element 4")
}
}
.navigationBarTitle(Text("Logs"), displayMode: .inline)
.navigationBarItems(leading: EditButton(), trailing: Button(action: {self.presentationMode.wrappedValue.dismiss()}) { Text("Done").bold()})
}
}
}
This has been working totally fine on iOS 13. However, in iOS 14 as you can see in my screenshot bellow it renders fully corrupted:
List elements have strange font size, color and are in upper-case (most important one!)
NavigationBar Buttons are greyed and in upper-case
NavigationBar title is in upper-case
The corrupted behaviour stays as long as you don't touch the screen. When you touch the screen and drag the sheet a little bit down then the list appearance will get corrected. If you do the same to the NavigationBar, it will then also be rendered correctly.
Is anybody also facing this issue? Any known fixes?
This looks like a bug. The possible workaround is to move sheet out of Form.
Tested with Xcode 12.0 / iOS 14.
struct ContentView: View {
#State var showSheet = false
var body: some View {
VStack{
Form{
Section(header:
headerView(showSheet: $showSheet)
) {
Text("Some Text")
}
}
}.sheet(isPresented: $showSheet) {sheetView()}
}
}
struct headerView: View {
#Binding var showSheet: Bool
var body: some View {
Button(action: { self.showSheet.toggle()}){
HStack{
Spacer()
Image(systemName: "pencil.and.ellipsis.rectangle")
Text("View Sheet")
}
}
}
}

ForEach onDelete Mac

I am trying to figure out how to delete from an array in swiftui for Mac. All the articles that I can find show how to do it the UIKit way with the .ondelete method added to the foreach. This is does not work for the Mac because I do not have the red delete button like iOS does. So how do I delete items from an array in ForEach on Mac.
here is the code that I have tried, which gives me the error
Fatal error: Index out of range: file
import SwiftUI
struct ContentView: View {
#State var splitItems:[SplitItem] = [
SplitItem(id: 0, name: "Item#1"),
SplitItem(id: 1, name: "Item#2")
]
var body: some View {
VStack {
Text("Value: \(self.splitItems[0].name)")
ForEach(self.splitItems.indices, id:\.self) { index in
HStack {
TextField("Item# ", text: self.$splitItems[index].name)
Button(action: {self.removeItem(index: index)}) {Text("-")}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func removeItem(index:Int) {
self.splitItems.remove(at: index)
}
}
struct SplitItem:Identifiable {
var id:Int
var name:String
}
Your ForEach code part is completely correct.
The possible error source could be the line
Text("Value: \(self.splitItems[0].name)") - after deleting all items this will definitely crash.
The next assumption is the VStack. After Drawing a view hierarchy it will not allow to change it.
If you replace the VStack with Group and the next code will work:
var body: some View {
Group {
if !self.splitItems.isEmpty {
Text("Value: \(self.splitItems[0].name)")
} else {
Text("List is empty")
}
ForEach(self.splitItems.indices, id:\.self) { index in
HStack {
TextField("Item# ", text: self.$splitItems[index].name)
Button(action: {self.removeItem(index: index)}) {Text("-")}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
so I was able to find the answer in this article,
instead of calling array.remove(at) by passing the index from the for loop call it using array.FirstIndex(where).