SwiftUI remove data into a structured array via a button in a ForEach - arrays

I'm trying to remove a line in my structured array when user click on the delete button. But as I use a foreach to load all my array lines into a specific subview I don't know how to pass the index of the ForEach into my subview to delete my line...
My code is like this,
ScrollView{
VStack {
ForEach(planeLibrary.testPlane){plane in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.white)
.shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
PlaneCellView(plane: plane, planeLibrary: planeLibrary, line: ???)
}
}
}.padding(.horizontal, 16)
}
And my PlaneCellView :
#State var plane: Plane
#ObservedObject var planeLibrary: PlaneLibrary
var line: Int
var body: some View {
//...
VStack(alignment: .leading) {
Text(plane.planeImat)
.font(.title)
.fontWeight(.bold)
Text(plane.planeType)
HStack{
Text(plane.isSe ? "SE" : "ME")
Text(plane.isNight ? "- Night" : "")
Text(plane.isIfr ? "- IFR" : "")
}
}
Spacer()
Button {
// HERE I don't know how to delete my array line ...
planeLibrary.testPlane.remove(at: line)
} label: {
Image(systemName: "trash.circle")
.foregroundColor(.red)
.font(.system(size: 30))
}
//...
}
My Plane library :
struct Plane: Identifiable{
let id = UUID().uuidString
let planeImat: String
let planeType: String
let isSe: Bool
let isIfr: Bool
let isNight: Bool
let autoID: String
init (planeImat: String, planeType: String, isSe: Bool, isIfr: Bool, isNight: Bool, autoID: String){
self.planeType = planeType
self.planeImat = planeImat
self.isSe = isSe
self.isIfr = isIfr
self.isNight = isNight
self.autoID = autoID
}
init(config: NewPlaneConfig){
self.planeImat = config.imat
self.planeType = config.type
self.isSe = config.isSe
self.isIfr = config.isIfr
self.isNight = config.isNight
self.autoID = config.autoID
}
}
I've already try to add id: \.self as I was able to find on this forum but without any success.

You haven't actually included PlaneLibrary, so I will assume that planeLibrary.testPlane is an array of Plane structs.
There are many ways of solving this, including changing testPlane to be a Dictionary of Plane structs (indexed by id), or if order is important, in an OrderedDictionary (add the swift-collections package to your project and import OrderedCollections in the file where it is used). You could use testPlane.removeValue(at: id) to remove the plane from either type of dictionary.
If you keep it as an array, but your array might be large and you're worried about run-time efficiency, the best thing to do is to change your ForEach to include the index of the planes in the loop.
It would look something like this:
ForEach(Array(planeLibrary.testPlane.enumerated()), id: \.element.id) { index, plane in
// In this code you can use either plane, or index.
...
// UI code
Text(plane.autoID)
...
{ // remove closure
planeLibrary.testPlane.remove(at: index)
}
}
But if the array is of reasonable size, you could keep it as it is now and use testPlane.remove(where:) to find it by id at the time of deletion. The code for this is much simpler and easier to read and understand, so it should probably be your first choice. Optimise for large lists later, if you need.

You can't pass the index in because that will crash the ForEach View. Instead, look up its index using its ID afterwards to remove it, e.g.
class RecipeBox: ObservableObject {
#Published var allRecipes: [Recipe]
#Published var collections: [String]
...
func delete(_ recipe: Recipe) {
delete(recipe.id)
}
func delete(_ id: Recipe.ID) {
if let index = index(for: id) {
allRecipes.remove(at: index)
updateCollectionsIfNeeded()
}
}
...
func index(for id: Recipe.ID) -> Int? {
allRecipes.firstIndex(where: { $0.id == id })
}
...
This sample is from Defining the source of truth using a custom binding (Apple Developer)

Related

AppStorage - Array - Error: No exact matches in call to initializer

I am developing an App for increasing productivity. My Main Goal in this file is to add Views over a dialog. Another target is to save the data in an Array for using it again with the annotation #AppStorage.
struct Task : Identifiable {
var id = UUID()
var myContent = "Empty"
var myCounter = 0
}
I'm using this struct to save my data which is here mainly the tasks name.
struct TaskView : View {
var task : Task
var body: some View {
HStack {
Spacer()
Text(String(task.myContent) ?? "test")
Spacer()
Text("Sessions today: " + String(task.myCounter))
Spacer()
Image(systemName: "xmark.bin.fill")
}
}
}
For displaying the data I'm using my own struct.
struct ItemList: View {
#AppStorage("myviews") var myviews : [Task]? = nil
#State private var showingAlert = false;
#State private var taskName = "tim";
var body: some View {
VStack{
if(!myviews.isEmpty){
for task in myviews {
TaskView(task: task)
}
}
Spacer()
Button {
showingAlert = true;
} label: {
Image(systemName: "plus")
.padding()
.background(Color.red)
.accentColor(.white)
.cornerRadius(100)
}
.alert(isPresented: $showingAlert) {
Alert(title: Text("Important message"), message: TextField("Task: "; text: $taskName), primaryButton: .destructive(Text("Got it!")){
myviews.append(Task(myContent: String(taskName), myCounter: 0))
})
}
Spacer()
}
}
}
So the main part consists of my #AppStorage Array, a loop to show existing "tasks" and a Dialog to add these tasks to the array.
The Error I am getting is the "No exact matches in call to initializer" directly in the line #AppStorage("myviews") var myviews : [Task]? = nil
I already tried different variations of initializing the array until I read in a forum that leaving the initialization not optional could be a cause to my problems.
Furthermore I checked my "Text" - Fields for the wrong types and casted the Int's (myCounter) to String.
It feels like I read every StackOverflow Article regarding my Error but none could help me.

SwiftUI custom nested array ForEach

So i have a nested array of a model:
let models = [[ButtonModel]]
struct ButtonModel: Identifiable {
let id = UUID()
let value: String
let style: ColorStyle
init(_ value: String, _ style: ColorStyle) {
self.value = value
self.style = style
}
}
Then i want to add this as a grid so i have a VStack in which i loop x amount of HStacks with buttons in it.
But because of some reason i get this error:
Cannot convert value of type '[[ButtonModel]]' to expected argument type 'Binding'
Generic parameter 'C' could not be inferred
VStack {
ForEach(viewModel.buttons, id: \.self) { buttons in
HStack(spacing: GridPoints.x2) {
Spacer()
ForEach(buttons) { buttonValue in
if buttonValue == "/" {
imageButton(for: Asset.Image) { viewModel.addValue(buttonValue) }
} else {
Button(buttonValue, action: { viewModel.addValue(buttonValue) })
.buttonStyle(customFont: .h3)
.background(Color.backgroundColor)
.styleText(style: TextStyle.h3)
}
}
Spacer()
}
.padding(GridPoints.x1)
}
}
Anyone know what this error is?
Edit
It looks like you made a mistake when using buttonValue too.
You should use it like this
ForEach(buttons) { button in
if button.value == "/" {
imageButton(for: Asset.Image) { viewModel.addValue(button.value) }
}
...
}
ForEach requires an array whose every element is Identifiable (or telling which one is the id to use using id: \.something).
Your problem is that if you use an array of array, this means that every element of the outer array is another array which is NOT conforming to Identifiable.
If you use ForEach(arrayOfArray, id: \.self), on the other hand, your telling that the identifier will be each element of the outer array that must conform to Hashable. Array conform to Hashable if its element does. So try to change your struct to
struct ButtonModel: Identifiable, Hashable { ... }
Alternate solution
You could change your structure to
struct ButtonsGroup: Identifiable {
let id = UUID()
let buttons: [ButtonModel]
}
Then you will be using it like this:
let groups = [ButtonsGroup]()
ForEach(groups) { group in
ForEach(group.buttons) { button in
Text(button.value)
}
}

Filtering a #Binding array var in a ForEach in SwiftUI returns values based on unfiltered array

I'm a Windows C# developer, new to iOS/SwiftUI development and I think I've worked myself into a hole here.
I have a view with a #Binding variable:
struct DetailView: View {
#Binding var project: Project
The project is an object which contains an array of Tasks. I am looping through the tasks of the project to display its name and a toggle whose state is determined by the Task's variable, isComplete.
ForEach(filteredTasks.indices, id: \.self) { idx in
HStack {
Text(filteredTasks[idx].phase)
.font(.caption)
Spacer()
Text(filteredTasks[idx].name)
Spacer()
Toggle("", isOn: self.$filteredTasks[idx].isComplete)
}
}
}
This took quite a while for me to get to this piece of code, and I found that I had to follow an example with the 'indices' option to get the toggle to work on each Task individually, and to make sure that its isComplete value was saved.
Next, I wanted to filter the list of Tasks based on a Task variable, phase, which has values of Planning, Construction, or Final. So I created 4 buttons (one for each phase, and then an 'All Tasks' to get back to the full, unfiltered list), and after a lot of trial and error (creating filtered arrays that no longer were bound correctly, etc., etc.) I tried this, basically working only with the original array.
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
HStack {
Text(project.tasks[idx].phase)
.font(.caption)
Spacer()
Text(project.tasks[idx].name)
Spacer()
Toggle("", isOn: self.$project.tasks[idx].isComplete)
}
}
}
And of course, this seemed to work because I can do a test:
func CreateTestArray() {
let testFilterArray = project.tasks.filter({ $0.phase.contains(filterValue) })
}
And that will give me the filtered list I want. However, in my ForEach view, it's not working correctly and I'm not sure how to work around it.
For example, I have 128 tasks, 10 of which have a value of 'Final' and when I use a button setting the filterValue to Final, the testFilterArray actually contains the correct 10 tasks - but in the ForEach view I'm getting the first ten tasks in the original array (which are of the type 'Planning' - the original array is sorted by Planning/Construction/Final); obviously the ForEach, in spite of the filter statement, is working on the original array. The Planning button sends the filterValue = "Planning", and I get the correct results because the filter returns 0-19 indices for the 20 Planning tasks I have in the original array, and since they're first in the original array, it 'appears' that the Planning filter is working correctly, tho in actually it's just by chance that it works, if the array were sorted differently it would not.
Any ideas how I can approach this so that I can actually filter on this array, display the isComplete toggle correctly for each item in the array, as well as update the toggle state dynamically? I feel like I need to start from scratch once again here because I've let these constraints work me into a tiny Swift corner.
Thanks!
Update:
Thank you, #jnpdx, for your quick response - and I should definitely have included objects (which I list below). However, looking back over my object definitions, I wonder if I've made an even more basic error in managing the objects which is why I've gotten boxed in to the situation I have (i.e., in earlier iterations I'd attempted some of your suggestion). At any rate, my published object is 'projects', which is a list of projects I pass to the project list view, and then that view passes a single project to a project view, and then that view lists the tasks in that particular project.
I feel like your answer is pointing me in the right decision, I just need to back back up and look at those object definitions/management and see how to get to a situation where a straightforward solution is possible.
Task:
struct Task: Identifiable, Codable {
let id: UUID
var phase: String
var category: String
var name: String
var isComplete: Bool
init(id: UUID = UUID(), phase: String, category: String, name: String, isComplete: Bool) {
self.id = id
self.phase = phase
self.category = category
self.name = name
self.isComplete = isComplete
}
}
The Project:
struct Project: Identifiable, Codable {
var id: UUID
var name: String
var type: String
var tasks: [Task]
var isComplete: Bool
init(id: UUID = UUID(), name: String, type: String, tasks: [Task] = [], isComplete: Bool) {
self.id = id
self.name = name
self.type = type
self.tasks = tasks
self.isComplete = isComplete
}
}
and the Project model:
class ProjectData: ObservableObject {
// code to access the json file is here
// An accessible list of projects from the saved file
#Published var projects: [Project] = []
// load and save functions follow
Update:
Thanks, #jnpdx, your solution worked after making, as you said I would need to, the tweaks to get it to function within my particular model design. Here are the snippets that finally worked in my case.
In my view:
List {
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) })) { task in
HStack {
Text(task.name)
Toggle("", isOn: self.makeBinding(item: task))
}
}
}
And the called function:
func makeBinding(item: Task) -> Binding<Bool> {
let i = self.project.tasks.firstIndex { $0.id == item.id }!
return .init(
get: { self.project.tasks[i].isComplete },
set: { self.project.tasks[i].isComplete = $0 }
)
}
Let's look at the following line from your code:
ForEach(project.tasks.filter({ $0.phase.contains(filterValue) }).indices, id: \.self) { idx in
In the first part, you filter tasks and then ask for the indices. My suspicion is that you're hoping it would return something like [1, 5, 10, 11, 12], meaning their original positions in the array. But, in reality, you're going to get a contiguous array like [0,1,2,3,4] because it's giving you indices from the newly-created array (the result of filter).
There are a couple of ways to solve this, which also relate to the previous ForEach that you had.
It's more idiomatic to do ForEach and iterate over structs/objects rather than indices. You don't show what Task is made up of, but let's say it's this:
struct Task : Hashable {
var id = UUID()
var name: String
var phrase: String
var isComplete: Bool
}
To iterate on it, you could do:
ForEach(task, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id)) //explained later
}
I asked about the type of Project in my comment because I'm not totally clear why it's a #Binding. It seems like maybe it's an object? If it's a view model, which would be nice, you can handle your Toggle logic there. Something like:
class Project : ObservableObject {
#Published var tasks : [Task] = [Task(name: "1", phrase: "phase", isComplete: false),Task(name: "2", phrase: "phase", isComplete: true),Task(name: "3", phrase: "phase2", isComplete: false)]
var completedTasks : [Task] {
return tasks.filter { $0.isComplete }
}
func taskCompletedBinding(id: UUID) -> Binding<Bool> {
Binding<Bool>(get: {
self.tasks.first(where: { $0.id == id})?.isComplete ?? false
}, set: { newValue in
self.tasks = self.tasks.map { t in
if t.id == id {
var tCopy = t
tCopy.isComplete = newValue
return tCopy
} else {
return t
}
}
})
}
}
And you can test that it works doing this:
struct ContentView: View {
#ObservedObject var project = Project()
var body: some View {
ForEach(project.tasks, id: \.id) { task in
Text(task.name)
Toggle("Done?", isOn: project.taskCompletedBinding(id: task.id))
}
}
}
If Project is a struct and not an object, it might be good to wrap it in an ObservableObject view model like I did above.

SwiftUI: List, ForEach, indices and .onDelete not working when using TextField() - (Index out of range) [duplicate]

Environment
Xcode 11.2.1 (11B500)
Problem
In order to implement editable teble with TextField on SwiftUI, I used ForEach(0..<items.count) to handle index.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i])
}
}
}
}
However, problems arise if the table is changed to be deleteable.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i]) // Thread 1: Fatal error: Index out of range
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐶" from animals
}
}
}
}
Thread 1: Fatal error: Index out of range when delete item
🐶 has been removed from animals and the ForEach loop seems to be running twice, even though animals.count is 1.
(lldb) po animals.count
1
(lldb) po animals
▿ 1 element
- 0 : "🐱"
Please give me advice on the combination of Foreach and TextField.
Thanks.
Ok, the reason is in documentation for used ForEach constructor (as you see range is constant, so ForEach grabs initial range and holds it):
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
So the solution would be to provide dynamic container. Below you can find a demo of possible approach.
Full module code
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐶", "🐱"]
var body: some View {
VStack {
HStack {
EditButton()
Button(action: { self.animals.append("Animal \(self.animals.count + 1)") }, label: {Text("Add")})
}
List {
ForEach(animals, id: \.self) { item in
EditorView(container: self.$animals, index: self.animals.firstIndex(of: item)!, text: item)
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐶" from animals
}
}
}
}
}
struct EditorView : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
}
}
it is because editbutton is IN your list. place it ouside or better in navigationbar.

Speed Up a list

I'm try to speed up my APP that for now unfortunately is very slow on performing some search and list of data with Swift Ui.
first of all I have a data model that describe my object airport, called AirportModel
import Foundation
class AirportModel : Identifiable , Codable{
var aptICAO : String
var aptName : String
var aptCity : String
var aptCountry :String
var aptIATA : String
init(aptICAO: String, aptName: String, aptCity: String, aptCountry: String, aptIATA: String) {
self.aptICAO = aptICAO
self.aptName = aptName
self.aptCity = aptCity
self.aptCountry = aptCountry
self.aptIATA = aptIATA
}
}
I have a local file apt.json, that contain all the data information for my airport (I downloaded from internet this json, inside there are around 29000 airport)
so when I run the first time the app, with the following function , i create and save the airportVector of type AirportModel.
func openfilejson (fileName : String) {
if let path = Bundle.main.path(forResource: fileName, ofType: "json") {
do {
let fileUrl = URL(fileURLWithPath: path)
let datafile = try Data(contentsOf: fileUrl,options: .mappedIfSafe)
let json = JSON(data:datafile)
objectWillChange.send()
for (key,_) in json {
let airport = AirportModel(aptICAO: "", aptName: "", aptCity: "", aptCountry: "", aptIATA: "")
airport.aptName = json[key]["name"].stringValue
airport.aptCity = json[key]["city"].stringValue
airport.aptCountry = json[key]["country"].stringValue
airport.aptIATA = json[key]["iata"].stringValue
airport.aptICAO = json[key].stringValue
airportVector.append(airport)
}
debugPrint("nel vettore airport Vector ci sono \(airportVector.count) aeroporti")
save2() // to save airport vector in the plistfile
debugPrint("SALVATO IN MEMORIA ")
} catch {
print("ERRORE OPEN FILE AEROPORTI")
}
}
}
all, this work fine , the vector with 29000 airport inside is created once the app is ru the first time.
NOW THE BIG ISSUE.
in one view, I try to list this airport using SwiftUI and a searchBar to search on it.
the problem is, due to the big amount of data to be opened, when I load the View with the list; the data take very long to be listed and moreover when I lunch the search everything is stuck!
any idea how can I process or speed up this huge amount of data to avoid the app stuck on the loading of the following view!
import SwiftUI
struct ContentView: View {
#ObservedObject var dm: DataManager
#State private var searchTerm : String = ""
var body: some View {
VStack {
Text("List")
SearchBar(text: $searchTerm).shadow(radius: 10)
List(dm.airportVector.filter{
$0.aptCity.localizedCaseInsensitiveContains(searchTerm)
|| $0.aptName.localizedCaseInsensitiveContains(searchTerm)
}){ item in
HStack {
Text(item.aptICAO).bold().font(Font.system(size:20)).frame(width: 90, height: 10)
//
Text(item.aptName).font(Font.system(size: 15))
Spacer()
VStack(alignment: .leading) {
Text(item.aptCity).font(Font.system(size: 10)).font(.subheadline)
Spacer()
Text(item.aptCountry).font(Font.system(size: 10))
}
}
}
}
}
}
Thanks in advance.
Damiano
Make your data conform to Identifiable and use ForEach inside of your List with a separate View to render the item:
List {
ForEach(items) {
MyItem(item)
}
}
Only render items when they change by making MyItem conform to Equatable, and define the == function. Make sure the == function is being executed by adding a breakpoint or print statement. If it's not being executed see this article for a workaround:
https://swiftui-lab.com/equatableview/
If the above suggestion works, but you find yourself making wholesale modifications to the list after initial render, try using id() during these modifications. See https://swiftui-lab.com/swiftui-id/
If the above suggestions don't improve things enough, you probably need a more custom solution. Use a TableView or CollectionView, see https://github.com/apptekstudios/ASCollectionView

Resources