How can I use onDisappear modifier for knowing the killed View in SwiftUI? - swift

I have a study project also you can call it testing/ learning project! Which has a goal to recreate the data source and trying get sinked with latest updates to data source!
In this project I was successful to create the same data source without sending or showing data source to the Custom View! like this example project in down, so I want keep the duplicated created data source with original data source updated!
For example: I am deleting an element in original data source and I am trying read the id of the View that get disappeared! for deleting the same one in duplicated one! but it does not work well!
The Goal: As you can see in my project and my codes! I want create and keep the duplicated data source sinked and equal to original without sending the data source directly to custom View! or even sending any kind of notification! the goal is that original data source should put zero work to make itself known to duplicated data source and the all work is belong to custom View to figure out the deleted item.
PS: please do not ask about use case or something like that! I am experimenting this method to see if it would work or where it could be helpful!
struct ContentView: View {
#State private var array: [Int] = Array(0...3)
var body: some View {
VStack(alignment: .leading, spacing: 10.0) {
ForEach(array.indices, id:\.self) { index in
CustomView(id: index) {
HStack {
Text(String(describing: array[index]))
.frame(width: 50.0)
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture { array.remove(at: index) }
}
}
}
}
.font(Font.body.bold())
}
}
struct CustomView<Content: View>: View {
let id: Int
let content: () -> Content
#StateObject private var customModel: CustomModel = CustomModel.shared
var body: some View {
content()
.preference(key: IntPreferenceKey.self, value: id)
.onPreferenceChange(IntPreferenceKey.self) { newValue in
if !customModel.array.contains(newValue) { customModel.array.append(newValue) }
}
.onDisappear(perform: {
print("id:", id, "is going get removed!")
customModel.array.remove(at: id)
})
}
}
class CustomModel: ObservableObject {
static let shared: CustomModel = CustomModel()
#Published var array: Array<Int> = Array<Int>() {
didSet {
print(array.sorted())
}
}
}
struct IntPreferenceKey: PreferenceKey {
static var defaultValue: Int { get { return Int() } }
static func reduce(value: inout Int, nextValue: () -> Int) { value = nextValue() }
}

Relying on array indexes for ForEach will be unreliable for this sort of work, since the ID you're using is self -- ie the index of the item. This will result in unreliable recalculations of the items in ForEach since they're not actually identifiable by their index. For example, if item 0 gets removed, then what was item 1 now becomes item 0, making using an index as the identifier relatively useless.
Instead, use actual unique IDs to describe your models and everything works as expected:
struct Item: Identifiable {
let id = UUID()
var label : Int
}
struct ContentView: View {
#State private var array: [Item] = [.init(label: 0),.init(label: 1),.init(label: 2),.init(label: 3)]
var body: some View {
VStack(alignment: .leading, spacing: 10.0) {
ForEach(array) { item in
CustomView(id: item.id) {
HStack {
Text("\(item.label) - \(item.id)")
.frame(width: 50.0)
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture { array.removeAll(where: { $0.id == item.id }) }
}
}
}
}
.font(.body.bold())
}
}
struct CustomView<Content: View>: View {
let id: UUID
let content: () -> Content
#StateObject private var customModel: CustomModel = CustomModel.shared
var body: some View {
content()
.preference(key: UUIDPreferenceKey.self, value: id)
.onPreferenceChange(UUIDPreferenceKey.self) { newValue in
if !customModel.array.contains(where: { $0 == id }) {
customModel.array.append(id)
}
}
.onDisappear(perform: {
print("id:", id, "is going to get removed!")
customModel.array.removeAll(where: { $0 == id })
})
}
}
class CustomModel: ObservableObject {
static let shared: CustomModel = CustomModel()
#Published var array: Array<UUID> = Array<UUID>() {
didSet {
print(array)
}
}
}
struct UUIDPreferenceKey: PreferenceKey {
static var defaultValue: UUID { get { return UUID() } }
static func reduce(value: inout UUID, nextValue: () -> UUID) { value = nextValue() }
}

Related

swift picker not selecting item

I have a short array of items that I want to display in a segmented picker. I'm passing the selected item (0, by default). The picker displays, but no item is selected, and the picker is unresponsive to clicks (in the simulator). I have a very similar picker that uses percentage values, and it works correctly. I am guessing that the issue has to do with the closure that I'm passing to the ForEach loop, but I am unclear on what syntax I should be using, if that is in fact the issue.
The code is as follows:
#State private var originalUnit = 0
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection $originalUnit) {
ForEach(sourceUnits, id: \.self {
Text($0)
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Any insights on this would be appreciated. Thanks in advance!
You have a type mismatch between your originalUnit (Int) and your sourceUnits (String). Your selection needs to match the type.
struct ContentView: View {
#State private var originalUnit = "meters" //<-- Here
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection: $originalUnit) {
ForEach(sourceUnits, id: \.self) {
Text($0)
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
If, for some reason, you really needed originalUnit to be an Int, you could use enumerated (normally not the most efficient method for large collections in a ForEach, but that'll be inconsequential for something this small) so that the id is the index and matches the type (Int) of originalUnit:
struct ContentView: View {
#State private var originalUnit = 0
let sourceUnits = ["meters","kilometers","feet","yards","miles"]
var body: some View {
NavigationView {
Form {
Section {
Picker("Unit", selection: $originalUnit) {
ForEach(Array(sourceUnits.enumerated()), id: \.0) { //<-- .0 is the index (Int)
Text($0.1) //<-- .1 is the original item String
}
} .pickerStyle(.segmented)
} header: {
Text("Choose Unit")
}
} .navigationTitle("MyApp")
}
}
}
You can set tag for your picker's row. Tag is any hashable type.
Refer this code work with any type of object for selection
struct TestView: View {
#StateObject var viewModel = TestViewModel()
var body: some View {
VStack {
Text(viewModel.selectedItem.title)
Picker("Select item", selection: $viewModel.selectedItem) {
ForEach(viewModel.items) { makeRowForItem($0) }
}
}
}
#ViewBuilder
func makeRowForItem(_ item: Item) -> some View {
Text(item.title).tag(item)
}
}
struct Item: Identifiable, Hashable {
var id = UUID().uuidString
var title = "Untitled"
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
class TestViewModel: ObservableObject {
#Published var selectedItem: Item
#Published var items: [Item]
init() {
let list = (1..<10).map { Item(title: "Untitled \($0)") }
items = list
selectedItem = list.first!
}
}

Create a dynamic list of editable objects in swiftUI, store in the mobile itself

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.

SwiftUI .onTapGesture issue in TableView with Sections and Rows

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

SwiftUI list empty state view/modifier

I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article

Swift View not updating when Observed Object changes

I have some code like this:
class Data: ObservableObject {
#Published var data = dbContent
init(){
let db = Firestore.firestore()
db.collection("collection").document(userID).addSnapshotListener {
//getting data from DB and storing them as objects by appending them to data
}
}
}
struct 1View: View {
#ObservedObject var myData: Data = Data()
var body: some View {
2View(myData: self.myData)
3View(myData: self.myData)
}
}
struct 2View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
struct 3View: View {
#State var myData: Data
var body: some View {
List(){
ForEach(data.count){ data in
Text(data)
}.onDelete(perform: deleteData) //Deletes the item
}
}
}
Now the issue is, that I can delete the the item in the 2View. This is then also shown and I implemented the functionality that it deletes the Item in the DB as well.
So the DB data gets altered but this is not shown in the 3View until I refresh it by e.g. revisiting it.
I have no idea what the cause is. Maybe I got a wrong understanding of #Published and ObservedObject ?
#State means that the view owns the data and manages the state. Try using #ObservedObject in your child views as well. Here is an example:
Model
struct Book: Codable, Identifiable {
#DocumentID var id: String?
var title: String
var author: String
var numberOfPages: Int
enum CodingKeys: String, CodingKey {
case id
case title
case author
case numberOfPages = "pages"
}
}
ViewModel
class BooksViewModel: ObservableObject {
#Published var books = [Book]()
private var db = Firestore.firestore()
private var listenerRegistration: ListenerRegistration?
private var cancellables = Set<AnyCancellable>()
init() {
fetchData()
}
deinit {
unregister()
}
func unregister() {
if listenerRegistration != nil {
listenerRegistration?.remove()
}
}
func fetchData() {
unregister()
listenerRegistration = db.collection("books").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.books = documents.compactMap { queryDocumentSnapshot -> Book? in
return try? queryDocumentSnapshot.data(as: Book.self)
}
}
}
func deleteBooks(at offsets: IndexSet) {
self.books.remove(atOffsets: offsets)
}
}
Views
import SwiftUI
struct SampleView: View {
#ObservedObject var viewModel = BooksViewModel()
var body: some View {
VStack {
InnerListView1(viewModel: viewModel)
InnerListView2(viewModel: viewModel)
}
}
}
struct InnerListView1: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List {
ForEach(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
.onDelete { indexSet in
self.viewModel.deleteBooks(at: indexSet)
}
}
}
}
struct InnerListView2: View {
#ObservedObject var viewModel: BooksViewModel
var body: some View {
List(viewModel.books) { book in
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.font(.subheadline)
Text("\(book.numberOfPages) pages")
.font(.subheadline)
}
}
}
}
One thing I noticed when trying to reproduce your issue: if you're using CodingKeys (which you only need to do if your the attribute names on the Firestore documents are different from the attribute names on your Swift structs), you need to make sure that the id is also included. Otherwise, id will be nil, which will result in the List view not being abel to tell the items apart.