I am trying to make a list of eatery locations each of which are displayed in an EateryRow which is able to be clicked to move to the EateryDetail page, however with the implementation of this code I get an error which I believe is related to the syntax of the NavigationLink argument.
Also: I found this question which seems to have the same problem as me but it remains unanswered.
import SwiftUI
struct EateryList: View {
#Binding var eateries: [Eatery]
var body: some View {
NavigationView {
VStack {
List {
ForEach(eateries) {
NavigationLink(destination: EateryDetail(eatery: $eateries[identifiedBy: $0])) { //error here
EateryRow(eatery: $eateries[identifiedBy: $0])
}
}
.onMove {
eateries.move(fromOffsets: $0, toOffset: $1)
EateriesApp.save()
}.onDelete {
eateries.remove(atOffsets: $0)
EateriesApp.save()
}
}
.navigationTitle("Favourite Eateries")
.navigationBarItems(leading: EditButton(), trailing: Button( action: add)
{
Image(systemName: "plus")
}
)
.listStyle(InsetGroupedListStyle())
}
}
}
func add() {
eateries.append(Eatery(name: "Eatery", location: "Insert location here", notes: "Insert notes here", reviews: ["Insert reviews here"], url: "https://i.imgur.com/y3MMnba.png"))
EateriesApp.save()
}
}
I get this error on the line with the NavigationLink:
Unnamed argument #2 must precede argument 'destination'
For further clarity this is how I've used the "eatery" variable in the EateryDetail and EatertyRow views:
struct EateryDetail: View {
#Binding var eatery: Eatery
struct EateryRow: View {
#Binding var eatery: Eatery
And here is my code for Eatery which is defined in a file called eateries.swift:
import Foundation
struct Eatery: Codable, Identifiable {
var id = UUID()
var name: String
var location: String
var notes: String
var reviews: [String] = []
var url: String = ""
}
In eateriesApp.swift this is also defined:
import SwiftUI
#main
struct EateriesApp: App {
#State var model: [Eatery] = EateriesApp.model
static var model: [Eatery] = {
guard let data = try? Data(contentsOf: EateriesApp.fileURL),
let model = try? JSONDecoder().decode([Eatery].self, from: data) else {
return [elCaminoCantina, theFineDine, nightBites, theRiverRodeo, theCozyKitchen, theElegantEatery]
}
return model
}()
static var modelBinding: Binding<[Eatery]>?
var body: some Scene {
EateriesApp.modelBinding = $model
return WindowGroup {
ContentView(eateries: $model)
}
}
You should need to use .indices in ForEach(eateries).
Like this
ForEach(eateries.indices) { index in
NavigationLink(destination: EateryDetail(eatery: $eateries[index])) { //error here
EateryRow(eatery: $eateries[index])
}
}
The problem is you are using a shorthand variable ($0). When you used $0 inside the NavigationLink then $0 is considered for NavigationLink not ForEach. so now both $0 are in conflict in your case.
You can check with the below code. In below code now not produce any error because now there is no use $0 inside the NavigationLink
ForEach(eateries) {
Text($0.name)
NavigationLink(destination: EateryDetail(eatery: $eateries[identifiedBy: $0])) {
Text("$0.name")
}
}
Another solution is to use one variable and store your $0 data like this.
ForEach(eateries) {
let eaterie = $0 //<--- Here
NavigationLink(destination: EateryDetail(eatery: $eateries[identifiedBy: eaterie])) { //<--- Here
EateryRow(eatery: $eateries[identifiedBy: eaterie]) //<--- Here
}
}
Related
I get to pass back data via closure, so new name is passed, but my UI is not updating. The new name of the user is printed when I go back to original view, but the text above the button is not getting that new value.
In my mind, updating startingUser should be enough to update the ContentView.
my ContentView:
#State private var startingUser: UserData?
var body: some View {
VStack {
Text(startingUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser?.name)")
}
}
}
my EditView:
struct DetailView: View {
#Environment(\.dismiss) var dismiss
var user: UserData
var callBackClosure: (UserData) -> Void
#State private var name: String
var body: some View {
NavigationView {
Form {
TextField("your name", text: $name)
}
.navigationTitle("edit view")
.toolbar {
Button("dismiss") {
var newData = self.user
newData.name = name
newData.id = UUID()
callBackClosure(newData)
dismiss()
}
}
}
}
init(user: UserData, callBackClosure: #escaping (UserData) -> Void ) {
self.user = user
self.callBackClosure = callBackClosure
_name = State(initialValue: user.name)
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(user: UserData.example) { _ in}
}
}
my model
struct UserData: Identifiable, Codable, Equatable {
var id = UUID()
var name: String
static let example = UserData(name: "Luke")
static func == (lhs: UserData, rhs: UserData) -> Bool {
lhs.id == rhs.id
}
}
update
using these changes solves the matter, but my question remains valid, cannot understand the right reason why old code not working, on other projects, where sheet and text depends on the same #state var it is working.
adding
#State private var show = false
adding
.onTapGesture {
startingUser = UserData(name: "Start User")
show = true
}
changing
.sheet(isPresented: $show) {
DetailView(user: startingUser ?? UserData.example) { newOnePassedFromWhatDoneInEDitView in
startingUser = newOnePassedFromWhatDoneInEDitView
print("✅ \(startingUser!.name)")
}
}
The reason Text is not showing you the updated user name that you are passing in the closure is, your startingUser property will be set to nil when you dismiss the sheet because you have bind that property with sheet. Now after calling callBackClosure(newData) you are calling dismiss() to dismiss the sheet. To overcome this issue you can try something like this.
struct ContentView: View {
#State private var startingUser: UserData?
#State private var updatedUser: UserData?
var body: some View {
VStack {
Text(updatedUser?.name ?? "no name")
Text("Create start user")
.onTapGesture {
startingUser = UserData(name: "Start User")
}
}
.sheet(item: $startingUser) { userToSend in
DetailView(user: userToSend) { newUser in
updatedUser = newUser
print("✅ \(updatedUser?.name ?? "no name")")
}
}
}
}
I would suggest you to read the Apple documentation of sheet(item:onDismiss:content:) and check the example from the Discussion section to get more understanding.
Xcode 12. code and lifecycle are Swiftui.
I've looked at other peoples related questions and it seems like they just dump their entire projects code on here; it's a bit overwhelming to pick what I need from it.
So, I've broken the issue down into the simplest example I can.
The goal is to have numberX iterate when I press the + button.
Thanks in advance!
struct InfoData: Identifiable {
var id = UUID()
var nameX: String
var numberX: Int
}
class ContentX: ObservableObject {
#Published var infoX = [
InfoData(nameX: "Example", numberX: 1)
]
}
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List(contentX.infoX) { item in
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
item.numberX += 1 //Eror shows up here <<
}
Text("\(item.numberX)")
}
}
}
}
In the syntax that you're using, item is an immutable value, as the error tells you. You can't mutate it, because it doesn't represent a true connection to the array it comes from -- it's just a temporary readable copy that is being used in the List iteration.
If you can upgrade to Xcode 13, you have access to something called element binding syntax in List and ForEach that lets you do this:
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List($contentX.infoX) { $item in //<-- Here
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
item.numberX += 1
}
Text("\(item.numberX)")
}
}
}
}
This gives you a Binding to item that is mutable, allowing you to change its value(s) and have them reflected in the original array.
Prior to Xcode 13/Swift 5.5, you'd have to define your own way to alter an element in the array. This is one solution:
class ContentX: ObservableObject {
#Published var infoX = [
InfoData(nameX: "Example", numberX: 1)
]
func alterItem(item: InfoData) {
self.infoX = self.infoX.map { $0.id == item.id ? item : $0 }
}
}
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List(contentX.infoX) { item in
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
var newItem = item
newItem.numberX += 1
contentX.alterItem(item: newItem)
}
Text("\(item.numberX)")
}
}
}
}
Or, another option where a custom Binding is used:
class ContentX: ObservableObject {
#Published var infoX = [
InfoData(nameX: "Example", numberX: 1)
]
func bindingForItem(item: InfoData) -> Binding<InfoData> {
.init {
self.infoX.first { $0.id == item.id }!
} set: { newValue in
self.infoX = self.infoX.map { $0.id == item.id ? newValue : $0 }
}
}
}
struct ContentView: View {
#StateObject var contentX = ContentX()
var body: some View {
List(contentX.infoX) { item in
HStack {
Text("\(item.nameX)")
Spacer()
Button("+") {
contentX.bindingForItem(item: item).wrappedValue.numberX += 1
}
Text("\(item.numberX)")
}
}
}
}
I tried to create a list of editable objects in SwiftUI. Here is my idea.
First of all, the editable item is as follows:
struct Item: Identifiable {
var id: UUID
var ItemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [
.init(id: .init(), ItemNum: 55),
.init(id: .init(), ItemNum: 57),
.init(id: .init(), ItemNum: 87)
]
}
After that I created a list that get data from the ItemStore:
struct ItemView: View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore
var body: some View {
NavigationView {
List {
ForEach(store.items.indexed(), id:\.1.id) {index, item in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
//.onAppear(perform: store.fetch) // want to fetch the data from the store whenever the list appear, however, no idea to perform the function?!
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
The editView:
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text(Text("Item Num: \(item.itemNum)"))
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
My question here:
I would like to fetch the data from 'store' onAppear. but it fails.
After I quit the app, all the previous data gone. How can I make them to keep inside my app, even the app is kill?
Your second question first: In terms of storing (persisting your data), you have many options. The easiest would be to store it in UserDefaults, which I'll show in my example. You could also choose to use CoreData, which would be more of a process to set up, but would give you a more robust solution later on. Many more options like Realm, Firebase, SQLite, etc. exist as well.
struct Item: Identifiable, Codable {
var id: UUID = UUID()
var itemNum: Int
var notes: String = ""
}
final class ItemStore: ObservableObject {
#Published var items: [Item] = [] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "savedItems")
}
}
}
let defaultValues : [Item] = [
.init(itemNum: 55),
.init(itemNum: 57),
.init(itemNum: 87)
]
func fetch() {
let decoder = JSONDecoder()
if let savedItems = UserDefaults.standard.object(forKey: "savedItems") as? Data,
let loadedItems = try? decoder.decode([Item].self, from: savedItems) {
items = loadedItems
} else {
items = defaultValues
}
}
}
struct ContentView : View {
#State private var editMode = EditMode.inactive
#ObservedObject var store: ItemStore = ItemStore()
var body: some View {
NavigationView {
List {
ForEach(Array(store.items.enumerated()), id:\.1.id) { (index,item) in
NavigationLink(destination: ItemEditingView(item: self.$store.items[index])) {
VStack(alignment: .leading) {
Text("Item Num: \(item.itemNum)")
}
}
}
}
.onAppear(perform: store.fetch)
.navigationBarTitle("Items")
.navigationBarItems( trailing: addButton)
.environment(\.editMode, $editMode)
}
}
private var addButton: some View {
switch editMode {
case .inactive:
return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
default:
return AnyView(EmptyView())
}
}
private func onAdd() {
store.items.append(Item(id: UUID(), itemNum: 10))
}
}
struct ItemEditingView: View {
#Environment(\.presentationMode) var presentation
#Binding var item: Item
var body: some View {
Form {
Section(header: Text("Item")) {
Text("Item Num: \(item.itemNum)")
TextField("Type something...", text: $item.notes)
}
Section {
Button("Save") {
self.presentation.wrappedValue.dismiss()
}
}
}.navigationTitle(Text("Item Num: \(item.itemNum)"))
}
}
Regarding your first question, the reason that fetch failed is you had no fetch method. Plus, there was nothing to fetch, since the array of items just got populated upon creation of the ItemStore each time.
Notes:
Item now conforms to Codable -- this is what allows it to get transformed into a value that can be saved/loaded from UserDefaults
fetch is now called on onAppear.
Every time the data is changed, didSet is called, saving the new data to UserDefaults
There were a number of typos and things that just plain wouldn't compile in the original code, so make sure that the changes are reflected. Some of those include: enumerated instead of indexed in the ForEach, not calling Text(Text( with nested values, using the same capitalization of itemNum throughout, etc
Important: when testing this, make sure to give the simulator a few seconds after a change to save the data into UserDefaults before killing the app and opening it again.
I have a piece of code where I'm trying to place Firestore data within a picker. I have made it previously so that the picker will show the Firestore data, but I am unable to select it to show in the 'selected view' therefore I rewrote the code and have the following error "Initializer 'init(_:)' requires that 'getSchoolData' conform to 'StringProtocol' "
Please excuse that it may sound like a daft question I just can't seem to solve it. A copy of my code is below. I have tried working on this for weeks but at a loss so please be kind, I'm new to coding.
Thanks in advance,
import SwiftUI
import Firebase
struct SchoolDetailsView: View {
let schoolData = [getSchoolData()]
#State var selectedSchool = 0
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(0 ..< schoolData.count) {
Text(self.schoolData[$0])
}
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("Select your school")
}
}
}
struct SchoolPicker_Previews: PreviewProvider {
static var previews: some View {
SchoolDetailsView()
}
}
class getSchoolData : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as! String
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable {
var id : String
var name : String
}
You may try the following:
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData() // make `#ObservedObject`/`#StateObject` instead of const array
#State var selectedSchool = "" // `schoolName.id` is of type String
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) { // choose whether you want to tag by `id` or by `name`
Text($0.name)
}
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("Select your school")
}
}
}
I have two models:
struct Category: Identifiable {
var id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
}
and:
struct ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked = false
}
with:
class Checklist: ObservableObject {
#Published var items = [Category]()
func deleteListItem(whichElement: IndexSet) {
items.remove(atOffsets: whichElement)
}
func moveListItem(whichElement: IndexSet, destination: Int) {
items.move(fromOffsets: whichElement, toOffset: destination)
}
}
I try to implement tap on row to check and uncheck cheklist item in tableView with sections and rows, but I cannot get how this can be released. My code:
struct ChecklistView: View {
#EnvironmentObject var checklist: Checklist
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}
.background(Color.white)
.onTapGesture {
if let matchingIndex =
category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].isChecked.toggle()
}
}
}
}
}
.onDelete(perform: checklist.deleteListItem)
.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: { self.newChecklistItemViewIsVisible = true }) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
NewChecklistItemView(checklist: self.checklist)
}
}
}
I get error with this code on line with category.items[matchingIndex].isChecked.toggle():
Cannot use mutating member on immutable value: 'category' is a 'let' constant
How I can get to ChecklistItem and make it check and uncheck on tap.
import SwiftUI
//Change to class and add NSObject structs are immutable
class Category: NSObject, Identifiable {
let id = UUID()
var title: String
var number: Int
var items: [ChecklistItem]
//Now you need an init
init(title: String , number: Int, items: [ChecklistItem]) {
self.title = title
self.number = number
self.items = items
}
}
//Change to class and add NSObject structs are immutable
class ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked: Bool = false
//Now you need an init
init(name: String) {
self.name = name
}
}
class Checklist: ObservableObject {
#Published var items = [Category]()
}
struct ChecklistView: View {
//Can be an #EnvironmentObject if the #ObservedObject comes from a higher View
#ObservedObject var checklist: Checklist = Checklist()
#State var newChecklistItemViewIsVisible = false
var body: some View {
NavigationView {
List {
ForEach(checklist.items) { category in
Section(header: Text(category.title)) {
ForEach(category.items) { item in
Button(action: {
print(item.isChecked.description)
item.isChecked.toggle()
//Something to trigger the view to refresh will not be necessary if using something like #FetchRequest or after you somehow notify `checklist.items` that there is a change
checklist.objectWillChange.send()
}) {
HStack {
Text(item.name)
Spacer()
Text(item.isChecked ? "✅" : "🔲")
}//HStack
//White is incompatible with Text Color in Dark Mode
.background(Color.gray)
}//Button
}//ForEach
}//Section
}//ForEach
//Methods not provided
//.onDelete(perform: checklist.deleteListItem)
//.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(
leading: Button(action: {
self.newChecklistItemViewIsVisible = true
//Code to Add Samples
checklist.items.append(Category(title: "Test", number: Int.random(in: 0...100), items: [ChecklistItem(name: "Test")]))
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("Add")
}
},
trailing: EditButton()
)
.navigationBarTitle("List")
}
.sheet(isPresented: $newChecklistItemViewIsVisible) {
//Pass as an #EnvironmentObject
NewChecklistItemView().environmentObject(checklist)
}
}
}
struct NewChecklistItemView: View {
#EnvironmentObject var checklist: Checklist
var body: some View {
Text(checklist.items.count.description)
}
}
struct ChecklistView_Previews: PreviewProvider {
static var previews: some View {
//When the #ObservedObject comes from a higher View remove comment below
ChecklistView()//.environmentObject(Checklist())
}
}
The reason you are getting that error is because structs are immutable. You should use method marked with "mutating" inside desired struct. Something like
if let matchingIndex = category.items.firstIndex(where: { $0.id == item.id }) {
category.items[matchingIndex].toggleItem()
}
and inside your struct:
mutating func toggleItem() {
self.isChecked.toggle()
}
But i would recommend you to use #State instead, because what you are trying to do is straight forward related to how you represent your view. And later, when user is willing to do something with that selection you send that data to your model