Get value of deleted item in onDelete function - list

I got a problem. I need the value of the item that got deleted with the .onDelete function. I am new to swift and i have no clue how to do it. It would be nice if someone could help me out here. Thanks.
The List:
struct TimeStampList: View {
#EnvironmentObject var user: User
#ObservedObject var viewModel: SingleEntriesViewModel
var body: some View {
List {
ForEach(0..<self.viewModel.timeStamps.count, id: \.self) { timeStamp in
Section(header: Text("\(TimeStamp.convertToReadableString(date: self.viewModel.timeStamps[timeStamp].date))")) {
if (self.viewModel.timeStamps[timeStamp].dayType != "V") {
ForEach(self.viewModel.timeStamps[timeStamp].times, id: \.self) { time in
SingleEntriesRow(time: time, id: UUID())
}.onDelete { times in
self.deleteTimeStamps(timeStamp: timeStamp, row: times)
}
}
else {
Text("V")
}
}
}
}
}
func deleteTimeStamps(timeStamp: Int, row: IndexSet) {
self.viewModel.timeStamps[timeStamp].deleteTime(at: row)
self.viewModel.sortTimeStamps()
self.viewModel.timeStamps[timeStamp].times[row] // <- I need this but row is of type IndexSet not Int.
}
}
The TimeStamp:
struct TimeStamp: Identifiable {
var id = UUID()
var date: Date
var times: [String]
var dayType: String
}
The ViewModel:
class SingleEntriesViewModel: ObservableObject {
#Published var timeStamps = [TimeStamp]()
}

This is because IndexSet is a collection of indices of type Int.
You can either get the first index:
if let index = row.first {
// index is of type `Int`
// let result = self.viewModel.timeStamps[timeStamp].times[index]
}
or use a forEach to iterate through all indices:
row.forEach { index in
// index is of type `Int`
// let result = self.viewModel.timeStamps[timeStamp].times[index]
}

Related

Left side of mutating operator isn't mutable: 'self' is immutable [duplicate]

Basically what i want to do is if you press the Button then entries should get a new CEntry. It would be nice if someone could help me out. Thanks!
struct AView: View {
var entries = [CEntries]()
var body: some View {
ZStack {
VStack {
Text("Hello")
ScrollView{
ForEach(entries) { entry in
VStack{
Text(entry.string1)
Text(entry.string2)
}
}
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp")) <-- Error
}) {
someButtonStyle()
}
}
}
}
The Class CEntries
class CEntries: ObservableObject, Identifiable{
#Published var string1 = ""
#Published var string2 = ""
init(string1: String, string2: String) {
self.string1 = string1
self.string2 = string2
}
}
Views are immutable in SwiftUI. You can only mutate their state, which is done by changing the properties that have a #State property wrapper:
#State var entries: [CEntries] = []
However, while you could do that, in your case CEntries is a class - i.e. a reference type - so while you could detect changes in the array of entries - additions and removals of elements, you won't be able to detect changes in the elements themselves, for example when .string1 property is updated.
And it doesn't help that it's an ObservableObject.
Instead, change CEntries to be a struct - a value type, so that if it changes, the value itself will change:
struct CEntries: Identifiable {
var id: UUID = .init()
var string1 = ""
var string2 = ""
}
struct AView: View {
#State var entries = [CEntries]()
var body: some View {
VStack() {
ForEach(entries) { entry in
VStack {
Text(entry.string1)
Text(entry.string2)
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp"))
}) {
someButtonStyle()
}
}
}
}

Using ForEach inside a Picker

I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'
The code is below:
import SwiftUI
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle, content: {
ForEach(0..<model.list.count, content: { index in
Text(index.style)
})
})
}
}
The model is here:
import Foundation
struct Bumps: Identifiable{
var id: String
var style: String
}
and the ViewModel is here:
import Foundation
import Firebase
import FirebaseFirestore
class ViewModel: ObservableObject {
#Published var list = [Bumps]()
#Published var styleArray = [String]()
func getData2() {
let db = Firestore.firestore()
db.collection("bumpStop").getDocuments { bumpSnapshot, error in
//Check for errors first:
if error == nil {
//Below ensures bumpSnapshot isn't nil
if let bumpSnapshot = bumpSnapshot {
DispatchQueue.main.async {
self.list = bumpSnapshot.documents.map{ bump in
return Bumps(id: bump.documentID,
style: bump["style"] as? String ?? "")
}
}
}
}
else {
//Take care of the error
}
}
}
}
index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedStyle = 0
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)}
Picker("Style", selection: $selectedStyle) {
ForEach(model.list.indices, id: \.self) { index in
Text(model.list[index].style).tag(index)
}
}
}
}
}
EDIT-1:
Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle.
However, as always when using index, you need to ensure it is valid at the time of use.
That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.
You could also use this alternative approach that does not use index:
struct Bumps: Identifiable, Hashable { // <-- here
var id: String
var style: String
}
struct BumpSelector: View {
#ObservedObject var model = ViewModel()
#State var selectedBumps = Bumps(id: "", style: "") // <-- here
init(){
model.getData2()
}
var body: some View {
VStack{
List (model.list) { item in
Text(item.style)
}
Picker("Style", selection: $selectedBumps) {
ForEach(model.list) { bumps in
Text(bumps.style).tag(bumps) // <-- here
}
}
}
.onAppear {
if let first = model.list.first {
selectedBumps = first
}
}
}
}
Then use selectedBumps, just like any Bumps, such as selectedBumps.style

iOS 15: Navigation link popping out, again

In the last few months, many developers have reported NavigationLinks to unexpectedly pop out and some workarounds have been published, including adding another empty link and adding .navigationViewStyle(StackNavigationViewStyle()) to the navigation view.
Here, I would like to demonstrate another situation under which a NavigationLink unexpectedly pops out:
When there are two levels of child views, i.e. parentView > childLevel1 > childLevel2, and childLevel2 modifies childLevel1, then, after going back from level 2 to level 1, level 1 pops out and parentView is shown.
I have filed a bug report but not heard from apple since. None of the known workarounds seem to work. Does someone have an idea what to make of this? Just wait for iOS 15.1?
Below is my code (iPhone app). In the parent view, there is a list of persons from which orders are taken. In childLevel1, all orders from a particular person are shown. Each order can be modified by clicking on it, which leads to childLevel2. In childLevel2, several options are available (here only one is shown for the sake of brevity), which is the reason why the user is supposed to leave childLevel2 via "< Back".
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var paId: Int
var n: Int // used only in the real code
}
class Data : ObservableObject {
init() {
pastries = [
Pastry(id: 0, name: "Prezel"),
Pastry(id: 1, name: "Donut"),
Pastry(id: 2, name: "bagel"),
Pastry(id: 3, name: "cheese cake"),
]
persons = [
Person(id: 0, name: "Alice", orders: [Order(paId: 1, n: 1)]),
Person(id: 1, name: "Bob", orders: [Order(paId: 2, n: 1), Order(paId: 3, n: 1)])
]
activePersonsIds = [0, 1]
}
#Published var activePersonsIds: [Int] = []
#Published var persons: [Person] = []
#Published var pastries: [Pastry]
#Published var latestOrder = Order(paId: 0, n: 1)
lazy var pastryName: (Int) -> String = { (paId: Int) -> String in
if self.pastries.first(where: { $0.id == paId }) == nil {
return "undefined pastryId " + String(paId)
}
return self.pastries.first(where: { $0.id == paId })!.name
}
var activePersons : [Person] {
return activePersonsIds.compactMap {id in persons.first(where: {$0.id == id})}
}
}
#main
struct Bretzel_ProApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
#StateObject var data = Data()
var body: some View {
TabView1(data: data)
// in the real code, there are more tabs
}
}
struct TabView1: View {
#StateObject var data: Data
var body: some View {
NavigationView {
List {
ForEach(data.activePersons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
VStack (alignment: .leading) {
Text(person.name)
}
}
)
}
}
.listStyle(PlainListStyle())
.navigationTitle("Orders")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
List() {
ForEach (0...p.orders.count-1, id: \.self) { loop in
Section(header:
HStack() {
Text("BESTELLUNG " + String(loop+1))
}
) {
EPSubview1(data: data, psId: psId, loop: loop)
}
}
}.navigationTitle(p.name)
.listStyle(InsetGroupedListStyle())
}
}
struct EPSubview1: View {
#ObservedObject var data: Data
var psId: Int
var loop: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
let o1: Order = p.orders[loop]
NavigationLink(
destination: SelectPastry(data: data)
.onAppear() {
data.latestOrder.paId = o1.paId
}
.onDisappear() {
data.persons[pindex].orders[loop].paId = data.latestOrder.paId
},
label: {
VStack(alignment: .leading) {
Text(String(o1.n) + " x " + data.pastryName(o1.paId))
}
}
)
}
}
struct SelectPastry: View {
#ObservedObject var data : Data
var body: some View {
VStack {
List {
ForEach(data.pastries, id: \.self) {pastry in
Button(action: {
data.latestOrder.paId = pastry.id
}) {
Text(pastry.name)
.foregroundColor(data.latestOrder.paId == pastry.id ? .primary : .secondary)
}
}
}.listStyle(PlainListStyle())
}
}
}
The problem is your ForEach. Despite that fact that Person conforms to Identifiable, you're using \.self to identify the data. Because of that, every time an aspect of the Person changes, so does the value of self.
Instead, just use this form, which uses the id vended by Identifiable:
ForEach(data.activePersons) { person in
Which is equivalent to:
ForEach(data.activePersons, id: \.id) { person in

Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift when removing element from array

I have a form to select some users and assign them a int value.
The model:
class ReadingTime: Identifiable, Hashable {
var id: Int
#State var user: User
#Published var value: Int
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func == (lhs: ReadingTime, rhs: ReadingTime) -> Bool {
lhs.id == rhs.id
}
init(id: Int, user: User, value: Int) {
self.id = id
self._user = State(wrappedValue: user)
self.value = value
}
}
The view:
#Binding var times: [ReadingTime]
#State var newUser: User?
func didSelect(_ user: User?) {
if let user = user {
readingTime.append(ReadingTime(id: readingTime.nextMaxId,
user: user,
value: 0))
}
}
// In the body:
VStack(alignment: .leading, spacing: 0) {
HStack {
Picker("Select a user", selection: $newUser.onChange(didSelect)) {
ForEach(users) {
Text($0.name).tag(Optional($0))
}
}
.id(users)
}
VStack(spacing: 8) {
ForEach(0..<times.count, id: \.self) { i in
HStack(spacing: 0) {
Text(times[i].user.name)
TextField("ms", value: $times[i].value, formatter: NumberFormatter())
Button(action: {
NSApp.keyWindow?.makeFirstResponder(nil)
if let index = times.firstIndex(where: { $0 == times[i] }) {
times.remove(at: index)
}
newUser = nil
}, label: {
Text("REMOVE")
})
}
}
}
}
}
}
It looks like this:
However, when deleting an entry in the list, I get this error:
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift
What's going on here?
Modifying the number of items of an array while being enumerated is an evil trap.
0..<times.count creates a temporary static range.
If you remove the first item in the array index 1 becomes 0, index 2 becomes 1 and so on.
Unfortunately there is no index times.count-1 anymore and you get the Index out of range crash when the loop reaches the last index.
You can avoid the crash if you enumerate the array reversed.
Since there is no accepted answer:
Don't use the index of your array to get the specific object inside your ForEach. Like you did before, you're able to access your objects without using count. Using count causes your Index out of range error.
Do it like this:
ForEach(times) { time in
// ...
Text(time.user.name)
// ...
}

Even and Odd rows using ForEach (SwiftUI)

What is the best way to distinguish even and odd rows in ForEach loop? Contents of the loop is not numbers (i.e. User struct), and it can be filtered using search phrase (using just index of item in array is not applicable in that way, I think). I need to change a color of that rows.
If I understand question correctly, you can use indices of your data array and check condition index % 2 == 1 (because indices begins from 0) for odd rows. For filtered data I suggest computed value:
import SwiftUI
import Combine
struct HighlightingRowData: Identifiable {
let id = UUID()
let title: String
}
final class SomeData: ObservableObject {
#Published var data: [HighlightingRowData] = [HighlightingRowData(title: "R. Martin"), HighlightingRowData(title: "McConell"), HighlightingRowData(title: "London"), HighlightingRowData(title: "London")]
}
struct HighlitedRowsInList: View {
#EnvironmentObject var someData: SomeData
#State private var searchedText = ""
private var filteredData: [HighlightingRowData] {
return searchedText == "" ? someData.data : someData.data.filter { $0.title.contains(searchedText) }
}
var body: some View {
List {
TextField("filter", text: $searchedText)
ForEach(filteredData.indices, id: \.self) { rowIndex in
HStack {
Text(self.filteredData[rowIndex].title)
Spacer()
}
.background(rowIndex % 2 == 1 ? Color.yellow : Color.clear)
}
}
}
}
struct HighlitedRowsInList_Previews: PreviewProvider {
static var previews: some View {
HighlitedRowsInList()
.environmentObject(SomeData())
}
}
you can achieve something like this with this code: