SwiftUI memory leak - swift

I'm getting a weird memory leak in SwiftUI when using List and id: \.self, where only some of the items are destroyed. I'm using macOS Monterey Beta 5.
Here is how to reproduce:
Create a new blank SwiftUI macOS project
Paste the following code:
class Model: ObservableObject {
#Published var objs = (1..<100).map { TestObj(text: "\($0)")}
}
class TestObj: Hashable {
let text: String
static var numDestroyed = 0
init(text: String) {
self.text = text
}
static func == (lhs: TestObj, rhs: TestObj) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
deinit {
TestObj.numDestroyed += 1
print("Deinit: \(TestObj.numDestroyed)")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
List(model.objs, id: \.self) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
}
}
Run the app, and press the "Remove half" button. Keep pressing it until all the items are gone. However, if you look at the console, you'll see that only 85 items have been destroyed, while there were 99 items. The Xcode memory graph also supports this.
This seems to be caused by the id: \.self line. Removing it and switching it out for id: \.text fixes the problem.
However the reason I use id: \.self is because I want to support multiple selection, and I want the selection to be of type Set<TestObj>, instead of Set<UUID>.
Is there any way to solve this issue?

If you didn't have to use selection in your List, you could use any unique & constant id, for example:
class TestObj: Hashable, Identifiable {
let id = UUID()
/* ... */
}
And then your List with the implicit id: \.id:
List(model.objs) { obj in
Text(obj.text)
}
This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.
But you need selection in List, so see more below about how to achieve that.
To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.
Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:
extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}
Change your Model object to this:
class Model: ObservableObject {
#Published var objs: OrderedDictionary<UUID, TestObj>
init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}
And rather than model.objs you'll use model.objs.values, but that's it!
See full demo code below to test the selection:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selection: Set<UUID> = []
var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}
print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}
Result:

Related

Binding Array of Structs with Strings in an Array of Structs

I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}

How to modify a user input inside a SwiftUI form loop

I'm developing a simple SwiftUI app in Xcode 11. I want to have a form that loops through multiple user input strings and displays a form with a button. When the user presses the button it modifies the input value - specifically increment or decrement it.
However when passing an array of references like UserInput().foo where UserInput is a published observable object I cannot modify the value inside a ForEach because the ForEach is passed a copy as oppose to the original reference (at least that's my basic understanding). How do I then try to achieve it? I read about inout and everybody says to avoid it but surely this must be a relatively common issue.
I've made an simple example of what I'm trying to do but I can't quite work it out:
import SwiftUI
class UserInput: ObservableObject {
#Published var foo: String = ""
#Published var bar: String = ""
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView?{
var userinputs = [
[UserInput().foo, "Foo"],
[UserInput().bar, "Bar"]
]
var inputs: some View{
VStack(){
ForEach(userinputs, id: \.self){userinput in
Text("\(userinput[1]): \(String(userinput[0]))")
Button(action: {
increment(input: String(userinput[0]))
}){
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String){
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
As I understood, when adding a value to userinputs, the ForEach values doesn't change.
Well, if that's the case, first of all, you could try creating a struct and in it, you declare foo and bar, then just declare a variable of type the struct. It'll look like this:
struct Input: Identifiable {
var id = UUID()
var foo: String
var bar: String
}
class UserInput: ObservableObject {
#Published var inputs: [Input] = [Input]()
}
//ContentView
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
LoopInputs()
}
func LoopInputs() -> AnyView? {
var inputs: some View {
VStack {
ForEach(input.inputs) { userinput in
Text("\(userinput.bar): \(String(userinput.foo))")
Button(action: {
increment(input: String(userinput.foo))
}) {
Text("Increase")
}
}
}
}
return AnyView(inputs)
}
func increment(input: String) {
var lead = Int(input) ?? 0
lead += 1
// input = String(lead)
}
}
Wouldn't this be easier and more elegant?

SwiftUI / Combine : Listening array items value change

I want to display multiple text fields, representing scores of each part of a match.
Example : For a volleyball match, we have 25/20, 25/22, 25/23. The global score is 3/0.
The global components architecture :
>> ParentComponent
>> MainComponent
>> X TextFieldsComponent (2 text fields, home/visitor score)
The lowest component, TextFieldsComponent, contains basic bindings :
struct TextFieldsComponent: View {
#ObservedObject var model: Model
class Model: ObservableObject, Identifiable, CustomStringConvertible {
let id: String
#Published var firstScore: String
#Published var secondScore: String
var description: String {
"\(firstScore) \(secondScore)"
}
init(id: String, firstScore: String = .empty, secondScore: String = .empty) {
self.id = id
self.firstScore = firstScore
self.secondScore = secondScore
}
}
var body: some View {
HStack {
TextField("Dom.", text: $model.firstScore)
.keyboardType(.numberPad)
TextField("Ext.", text: $model.secondScore)
.keyboardType(.numberPad)
}
}
}
The parent component needs to show the total score of all parts of the match. And I wanted to try a Combine binding/stream to get the total score.
I tried multiple solutions and I ended up with this non-working code (the reduce seems to not be take all the elements of the array but internally stores a previous result) :
struct MainComponent: View {
#ObservedObject var model: Model
#ObservedObject private var totalScoreModel: TotalScoreModel
class Model: ObservableObject {
#Published var scores: [TextFieldsComponent.Model]
init(scores: [TextFieldsComponent.Model] = [TextFieldsComponent.Model(id: "main")]) {
self.scores = scores
}
}
private final class TotalScoreModel: ObservableObject {
#Published var totalScore: String = ""
private var cancellable: AnyCancellable?
init(publisher: AnyPublisher<String, Never>) {
cancellable = publisher.print().sink {
self.totalScore = $0
}
}
}
init(model: Model) {
self.model = model
totalScoreModel = TotalScoreModel(
publisher: Publishers.MergeMany(
model.scores.map {
Publishers.CombineLatest($0.$firstScore, $0.$secondScore)
.map { ($0.0, $0.1) }
.eraseToAnyPublisher()
}
)
.reduce((0, 0), { previous, next in
guard let first = Int(next.0), let second = Int(next.1) else { return previous }
return (
previous.0 + (first == second ? 0 : (first > second ? 1 : 0)),
previous.1 + (first == second ? 0 : (first > second ? 0 : 1))
)
})
.map { "[\($0.0)] - [\($0.1)]" }
.eraseToAnyPublisher()
)
}
var body: some View {
VStack {
Text(totalScoreModel.totalScore)
ForEach(model.scores) { score in
TextFieldsComponent(model: score)
}
}
}
}
I'm searching for a solution to get an event on each binding change, and merge it in a single stream, to display it in MainComponent.
N/B: The TextFieldsComponent needs to be usable in standalone too.
MergeMany is the correct approach here, as you started out yourself, though I think you overcomplicated things.
If you want to display the total score in the View (and let's say the total score is "owned" by Model instead of TotalScoreModel, which makes sense since it owns the underlying scores), you'd then need to signal that this model will change when any of the underlying scores will change.
Then you can provide the total score as a computed property, and SwiftUI will read the updated value when it recreates the view.
class Model: ObservableObject {
#Published var scores: [TextFieldsComponent.Model]
var totalScore: (Int, Int) {
scores.map { ($0.firstScore, $0.secondScore) }
.reduce((0,0)) { $1.0 > $1.1 ? ( $0.0 + 1, $0.1 ) : ($0.0, $0.1 + 1) }
}
private var cancellables = Set<AnyCancellable>()
init(scores: [TextFieldsComponent.Model] = [.init(id: "main")]) {
self.scores = scores
// get the ObservableObjectPublisher publishers
let observables = scores.map { $0.objectWillChange }
// notify that this object will change when any of the scores change
Publishers.MergeMany(observables)
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
Then, in the View, you can just use the Model.totalScore as usual:
#ObservedObject var model: Model
var body: some View {
Text(model.totalScore)
}

Get index in ForEach in SwiftUI

I have an array and I want to iterate through it initialize views based on array value, and want to perform action based on array item index
When I iterate through objects
ForEach(array, id: \.self) { item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Can't get index, so this won't work
}
}
So, I've tried another approach
ForEach((0..<array.count)) { index in
CustomView(item: array[index])
.tapAction {
self.doSomething(index)
}
}
But the issue with second approach is, that when I change array, for example, if doSomething does following
self.array = [1,2,3]
views in ForEach do not change, even if values are changed. I believe, that happens because array.count haven't changed.
Is there a solution for this?
Another approach is to use:
enumerated()
ForEach(Array(array.enumerated()), id: \.offset) { index, element in
// ...
}
Source: https://alejandromp.com/blog/swiftui-enumerated/
This works for me:
Using Range and Count
struct ContentView: View {
#State private var array = [1, 1, 2]
func doSomething(index: Int) {
self.array = [1, 2, 3]
}
var body: some View {
ForEach(0..<array.count) { i in
Text("\(self.array[i])")
.onTapGesture { self.doSomething(index: i) }
}
}
}
Using Array's Indices
The indices property is a range of numbers.
struct ContentView: View {
#State private var array = [1, 1, 2]
func doSomething(index: Int) {
self.array = [1, 2, 3]
}
var body: some View {
ForEach(array.indices) { i in
Text("\(self.array[i])")
.onTapGesture { self.doSomething(index: i) }
}
}
}
I usually use enumerated to get a pair of index and element with the element as the id
ForEach(Array(array.enumerated()), id: \.element) { index, element in
Text("\(index)")
Text(element.description)
}
For a more reusable component, you can visit this article https://onmyway133.com/posts/how-to-use-foreach-with-indices-in-swiftui/
I needed a more generic solution, that could work on all kind of data (that implements RandomAccessCollection), and also prevent undefined behavior by using ranges.
I ended up with the following:
public struct ForEachWithIndex<Data: RandomAccessCollection, ID: Hashable, Content: View>: View {
public var data: Data
public var content: (_ index: Data.Index, _ element: Data.Element) -> Content
var id: KeyPath<Data.Element, ID>
public init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (_ index: Data.Index, _ element: Data.Element) -> Content) {
self.data = data
self.id = id
self.content = content
}
public var body: some View {
ForEach(
zip(self.data.indices, self.data).map { index, element in
IndexInfo(
index: index,
id: self.id,
element: element
)
},
id: \.elementID
) { indexInfo in
self.content(indexInfo.index, indexInfo.element)
}
}
}
extension ForEachWithIndex where ID == Data.Element.ID, Content: View, Data.Element: Identifiable {
public init(_ data: Data, #ViewBuilder content: #escaping (_ index: Data.Index, _ element: Data.Element) -> Content) {
self.init(data, id: \.id, content: content)
}
}
extension ForEachWithIndex: DynamicViewContent where Content: View {
}
private struct IndexInfo<Index, Element, ID: Hashable>: Hashable {
let index: Index
let id: KeyPath<Element, ID>
let element: Element
var elementID: ID {
self.element[keyPath: self.id]
}
static func == (_ lhs: IndexInfo, _ rhs: IndexInfo) -> Bool {
lhs.elementID == rhs.elementID
}
func hash(into hasher: inout Hasher) {
self.elementID.hash(into: &hasher)
}
}
This way, the original code in the question can just be replaced by:
ForEachWithIndex(array, id: \.self) { index, item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Now works
}
}
To get the index as well as the element.
Note that the API is mirrored to that of SwiftUI - this means that the initializer with the id parameter's content closure is not a #ViewBuilder.
The only change from that is the id parameter is visible and can be changed
For non zero based arrays avoid using enumerated, instead use zip:
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
// Add Code here
}
I created a dedicated View for this purpose:
struct EnumeratedForEach<ItemType, ContentView: View>: View {
let data: [ItemType]
let content: (Int, ItemType) -> ContentView
init(_ data: [ItemType], #ViewBuilder content: #escaping (Int, ItemType) -> ContentView) {
self.data = data
self.content = content
}
var body: some View {
ForEach(Array(zip(data.indices, data)), id: \.0) { idx, item in
content(idx, item)
}
}
}
Now you can use it like this:
EnumeratedForEach(items) { idx, item in
...
}
ForEach is SwiftUI isn’t the same as a for loop, it’s actually doing something called structural identity. The documentation of ForEach states:
/// It's important that the `id` of a data element doesn't change, unless
/// SwiftUI considers the data element to have been replaced with a new data
/// element that has a new identity.
This means we cannot use indices, enumerated or a new Array in the ForEach. The ForEach must be given the actual array of identifiable items. This is so SwiftUI can animate the rows around to match the data, obviously this can't work with indicies, e.g. if row at 0 is moved to 1 its index is still 0.
To solve your problem of getting the index, you simply have to look up the index like this:
ForEach(items) { item in
CustomView(item: item)
.tapAction {
if let index = array.firstIndex(where: { $0.id == item.id }) {
self.doSomething(index)
}
}
}
You can see Apple doing this in their Scrumdinger sample app tutorial.
guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else {
fatalError("Can't find scrum in array")
}
The advantage of the following approach is that the views in ForEach even change if state values ​​change:
struct ContentView: View {
#State private var array = [1, 2, 3]
func doSomething(index: Int) {
self.array[index] = Int.random(in: 1..<100)
}
var body: some View {
let arrayIndexed = array.enumerated().map({ $0 })
return List(arrayIndexed, id: \.element) { index, item in
Text("\(item)")
.padding(20)
.background(Color.green)
.onTapGesture {
self.doSomething(index: index)
}
}
}
}
... this can also be used, for example, to remove the last divider
in a list:
struct ContentView: View {
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
let arrayIndexed = [Int](1...5).enumerated().map({ $0 })
return List(arrayIndexed, id: \.element) { index, number in
VStack(alignment: .leading) {
Text("\(number)")
if index < arrayIndexed.count - 1 {
Divider()
}
}
}
}
}
2021 solution if you use non zero based arrays avoid using enumerated:
ForEach(array.indices,id:\.self) { index in
VStack {
Text(array[index].name)
.customFont(name: "STC", style: .headline)
.foregroundColor(Color.themeTitle)
}
}
}
To get indexing from SwiftUI's ForEach loop, you could use closure's shorthand argument names:
#State private var cars = ["Aurus","Bentley","Cadillac","Genesis"]
var body: some View {
NavigationView {
List {
ForEach(Array(cars.enumerated()), id: \.offset) {
Text("\($0.element) at \($0.offset) index")
}
}
}
}
Results:
// Aurus at 0 index
// Bentley at 1 index
// Cadillac at 2 index
// Genesis at 3 index
P. S.
Initially, I posted an answer with a "common" expression that all Swift developers are used to, however, thanks to #loremipsum I changed it. As stated in WWDC 2021 Demystify SwiftUI video (time 33:40), array indices are not stable from \.self identity (key path).
ForEach(0 ..< cars.count, id: \.self) { // – NOT STABLE
Text("\(cars[$0]) at \($0) index")
}
Here is a simple solution though quite inefficient to the ones above..
In your Tap Action, pass through your item
.tapAction {
var index = self.getPosition(item)
}
Then create a function the finds the index of that item by comparing the id
func getPosition(item: Item) -> Int {
for i in 0..<array.count {
if (array[i].id == item.id){
return i
}
}
return 0
}
You can use this method:
.enumerated()
From the Swift documentation:
Returns a sequence of pairs (n, x), where n represents a consecutive
integer starting at zero and x represents an element of the sequence.
var elements: [String] = ["element 1", "element 2", "element 3", "element 4"]
ForEach(Array(elements.enumerated()), id: \.element) { index, element in
Text("\(index) \(element)")
}
Just like they mentioned you can use array.indices for this purpose
BUT remember that indexes that you've got are started from last element of array, To fix this issue you must use this: array.indices.reversed() also you should provide an id for the ForEach.
Here's an example:
ForEach(array.indices.reversed(), id:\.self) { index in }

IndexSet referring to index of the section instead of the row

I want to implement a swipe-to-delete functionality on a SwiftUI Section by using the .onDelete modifier. The problem is that it always deletes the first item in the list.
My view has a list with dynamic sections created with a ForEach.
struct SetListView : View {
var setlist: Setlist
var body : some View {
List {
ForEach(setlist.sets) {
SetSection(number: $0.id, songs: $0.songs)
}
}
.listStyle(.grouped)
}
}
In each section, there is another ForEach to create the dynamic rows:
private struct SetSection : View {
var number: Int
#State var songs: [Song]
var body : some View {
Section (header: Text("Set \(number)"), footer: Spacer()) {
ForEach(songs) { song in
SongRow(song: song)
}
.onDelete { index in
self.songs.remove(at: index.first!)
}
}
}
}
While debugging, I found out that the IndexSet is referring to the current section instead of the row. So when deleting items from the first section, always the first item gets deleted (as the index for the first section is 0).
Is this a bug in SwiftUI?
If not, then how could I get the index for the row?
In simple terms, a solution to this problem is to pass the section to your deletion method by:
Adopting RandomAccessCollection on your source data.
Binding the section in your outer ForEach and then using it in your inner ForEach, passing it to your deletion method:
List {
ForEach(someGroups.indices) { section in
bind(self.someGroups[section]) { someGroup in
Section(header: Text(someGroup.displayName)) {
ForEach(someGroup.numbers) { number in
Text("\(number)")
}
.onDelete { self.delete(at: $0, in: section) }
}
}
}
}
func delete(at offsets: IndexSet, in section: Int) {
print("\(section), \(offsets.first!)")
}
A complete, contrived working example
(Also available in Gist form for convenience):
import SwiftUI
func bind<Value, Answer>(_ value: Value, to answer: (Value) -> Answer) -> Answer { answer(value) }
struct Example: View {
struct SomeGroup: Identifiable, RandomAccessCollection {
typealias Indices = CountableRange<Int>
public typealias Index = Int;
var id: Int
var displayName: String
var numbers: [Int]
public var endIndex: Index {
return numbers.count - 1
}
public var startIndex: Index {
return 0
}
public subscript(position: Int) -> Int {
get { return numbers[position] }
set { numbers[position] = newValue }
}
}
var someGroups: [SomeGroup] = {
return [
SomeGroup(id: 0, displayName: "First", numbers: [1, 2, 3, 4]),
SomeGroup(id: 1, displayName: "Second", numbers: [1, 3, 5, 7])
]
}()
var body: some View {
List {
ForEach(someGroups.indices) { section in
bind(self.someGroups[section]) { someGroup in
Section(header: Text(someGroup.displayName)) {
ForEach(someGroup.numbers) { number in
Text("\(number)")
}
.onDelete { self.delete(at: $0, in: section) }
}
}
}
}
.listStyle(.grouped)
}
func delete(at offsets: IndexSet, in section: Int) {
print("\(section), \(offsets.first!)")
}
}
Many thanks to #rob-mayoff who pointed me in the right direction for this solution via Twitter!
I had exactly the same problem. Turns out, SwiftUI's (current?) implementation does not recognize nested lists. This means that each SetSection in your List is interpreted as a single row even though you have a ForEach in it with the actual SongRows. Hence, it the IndexSet (index.first!) always returns zero.
What I've also noticed is that even with a flat hierarchy such as..
List {
Section {
ForEach(...) {
...
}
}
Section {
ForEach(...) {
...
}
}
}
..individual rows cannot be moved between sections. This is also true when directly using two ForEach, i.e. without the Section wrappers.
We should probably file a report for each phenomenon.
It seems to work fine on Xcode 12.5
I'm using it like this:
struct Sections: View {
var items: [SomeData]
private var sections: [Date: [SomeData]] {
Dictionary(grouping: items, by: { $0.date })
}
private var headers: [Date] {
sections.map({ $0.key }).sorted().reversed()
}
var body: some View {
List {
ForEach(headers, id: \.self) { date in
Section(header: Text(date.friendly) {
AList(items: sections[date]!)
}
}
}
}
}
struct AList: View {
var items: [SomeData]
var body: some View {
ForEach(items) { data in
...
}
.onDelete(perform: delete)
}
private func delete(at offsets: IndexSet) {
// You can use `items.remove(atOffsets: offsets)`
for offset in offsets {
let data = items[offset]
print("\(data)")
// You can check here that this is the item that you want to remove and then you need to remove it from your data source.
// I'm using Realm and #Published vars that works fine, you should adapt to your logic.
}
}
}