i try to combine two objects from the following types
#ObservedObject var expenses = Expense()
#ObservedObject var recipes = Recipe()
The arrays worked quite good and everything is fine.
Now i would like to present all items from the arrays in a ForEach.
var body: some View {
TabView {
NavigationView {
List {
ForEach(Array(zip(expenses.items, recipes.ReItems)),id: \.0){ item in
HStack{
VStack(alignment: .leading){
Text(item.beschreibung)
.font(.headline)
Text(String(item.menge) + " \(item.unitType)")
}
}
}
.onDelete(perform: removeItems)
}
But this throws an error - "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
My first idea was to save the arrays in a variable like in this stackoverflow post
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
#State private var arrayExpense = self.expenses.items
#State private var arrayRecipes = self.recipes.ReItems
But to be hoonest, this looks not good.. It also throws an exception ;o
Thanks for your help!
Try to break it apart, like below (this gives compilers explicitly type check result of zip)
var body: some View {
TabView {
NavigationView {
List {
self.listContent(items: Array(zip(expenses.items, recipes.ReItems)))
}
...
private func listContent(items: [Item]) -> some View {
ForEach(items, id: \.0){ item in
HStack{
VStack(alignment: .leading){
Text(item.beschreibung)
.font(.headline)
Text(String(item.menge) + " \(item.unitType)")
}
}
}
.onDelete(perform: removeItems)
}
should the Text be:
Text(item.0.beschreibung).font(.headline)
Text(String(item.0.menge) + " \(item.0.unitType)")
or with item.1 as the case maybe.
Related
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.
I have run into this issue in SwiftUI. I want to be able to remove an item from an Array when the user presses on a button, but I get a "Thread 1: Fatal error: Index out of range" error when I try. This seems to have to do with the fact that IntView takes in a #Binding: if I make num just a regular variable, the code works fine with no errors. Unfortunately, I need to be able to pass in a Binding to the view for my purposes (this is a simplified case), so I am not sure what I need to do so the Binding doesn't cause the bug.
Here is my code:
import SwiftUI
struct IntView: View {
#Binding var num: Int // if I make this "var num: Int", there are no bugs
var body: some View {
Text("\(num)")
}
}
struct ArrayBugView: View {
#State var array = Array(0...10)
var body: some View {
ForEach(array.indices, id: \.self) { num in
IntView(num: $array[num])
Button(action: {
self.array.remove(at: num)
}, label: {
Text("remove")
})
}
}
}
Any help is greatly appreciated!
In your code the ForEach with indicies and id: \.self is a mistake. The ForEach View in SwiftUI isnβt like a traditional for loop. 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 on the actual array of identifiable items. This is so SwiftUI can track the row Views moving around, which is called structural identity and you can learn about it in Demystify SwiftUI WWDC 2021.
So you have to change your code to something this:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var num: Int
}
struct IntView: View {
let num: Int
var body: some View {
Text("\(num)")
}
}
struct ArrayView: View {
#State var array: [Item] = [Item(num:0), Item(num:1), Item(num:2)]
var body: some View {
ForEach(array) { item in
IntView(num: item.num)
Button(action: {
if let index = array.firstIndex(where: { $0.id == item.id }) {
array.remoteAt(index)
}
}, label: {
Text("remove")
})
}
}
}
I am very new to programming in Swift. So I'm trying to come up with a time management program. I have posted some code that have been derived from my project that is a work in progress, and I'm trying to troubleshoot some issues that I'm having that come from my lack of knowledge regarding Swift and SwiftUI. I would like to ask two questions here, but if you only have the answer to just one of them, I would greatly appreciate it.
So in my ContentView, I'm trying to display the taskName of the object with ID 0 using a Text in a VStack -- however, it is not displaying, and I'm not sure of the reason why. I can display the taskLength by putting it inside the String method, but taskName is not coming up when I attempt to display it.
Also I'm attempting to change the taskName of Task(id: 0) that is being passed into display2 directly from the display2, but I'm not sure if the taskName of Task(id: 0) is actually being changed, or it's only the taskName of #State var task:Task in display2 that is being changed -- based on my intuitions, I would think the latter case is actually happening. In that case, is there a way to directly edit the taskName of Task(id: 0) from display2?
import SwiftUI
import Foundation
import Combine
struct Task: Hashable, Codable, Identifiable {
var id: Int
var taskName: String = ""
var taskLength: Int = 0
var isBreak : Bool = false
}
class ModelData : ObservableObject{
#Published var tasks: [Task] = [
Task(id: 0,taskName: "Test", taskLength: 34, isBreak: false),
Task(id: 1,taskName: "Math", taskLength: 30, isBreak: false),
Task(id: 2,taskName: "Science", taskLength: 40, isBreak: false)
]
}
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(Task(id: 0).taskName)
display2(task:Task(id: 0))
}
}
}
struct display2: View{
#State var task:Task
var body: some View {
TextField("New task",text: $task.taskName)
}
}
The problem is here:
Text(Task(id: 0).taskName)
Here, you're creating a new Task, with an id of 0. This is not the first task inside your ModelData's tasks array.
Instead, reference the first task via subscript []:
Text(modelData.tasks[ /* index of task */ ].taskName)
Normally you can just put 0 here to get the first Task. However, you said you actually want the Task with an id of 0. You can do this via firstIndex(where:).
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(
modelData.tasks[getTaskIndexFrom(id: 0)] /// access
.taskName
)
Display2( /// see https://stackoverflow.com/a/67064699/14351818
task: $modelData.tasks[getTaskIndexFrom(id: 0)]
)
}
}
func getTaskIndexFrom(id: Int) -> Int {
/// get first index of a task with the specified `id`
if let firstIndex = modelData.tasks.firstIndex(where: { $0.id == 0 }) {
return firstIndex
} else {
return 0
}
}
}
struct Display2: View{
#Binding var task: Task /// need a Binding here
var body: some View {
TextField("New task", text: $task.taskName)
}
}
Ok, your second question:
In that case, is there a way to directly edit the taskName of Task(id: 0) from display2?
Yep! Just use #Binding on Display2's task. This way, all changes will be synced back to your modelData.
In ContentView you used just Task(), but you have to use modelData for #Published var tasks in ModelData.
Task(id: 0).taskName -> modelData.tasks[1].taskName
struct ContentView: View {
#EnvironmentObject var modelData: ModelData
var body: some View {
VStack{
Text(modelData.tasks[1].taskName)
.foregroundColor(.blue)
display2(task:Task(id: 0))
}
}
}
Also, as long as you use #EnvironmentObject, you need to add .environmentObject to the main as well.
(The code below is an example of the SwiftUI life cycle)
import SwiftUI
#main
struct ReplyToStackoverflowApp: App {
var modelData: ModelData = ModelData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(modelData)
}
}
}
I start by adding some integers to an array in onAppear for my outermost stack. But when I try to display the contents of the array using ForEach, I get an index out of range error.
struct MyView: View {
#State private var answers = [Int]()
var body: some View {
VStack {
ForEach(0..<4) { number in
Text("\(answers[number])")
}
}
.onAppear {
for _ in (0..<4) {
answerArray.append(Int.random(in: 1...10))
}
}
Never retrieve items by hard-coded indices in a ForEach expression.
Do count the array, the loop is skipped (safely) if the array is empty.
ForEach(0..<answers.count) { number in
Or - still simpler β enumerate the items rather than the indices
ForEach(answers, id: \.self) { answer in
Text("\(answer)")
}
onAppear is called after MyView loads for the first time, and at that moment, answers is still empty. That's why your program crashes at ForEach(0..<4), because answers's doesn't have 4 elements yet.
ForEach(0..<4) { number in
Text("\(answers[number])") /// answers is still empty.
}
Instead, you should look over answers.indices, so that answers[number] is guaranteed to exist. Make sure to also provide an id (id: \.self) to satisfy ForEach's Identifiable requirement.
struct MyView: View {
#State private var answers = [Int]()
var body: some View {
VStack {
ForEach(answers.indices, id: \.self) { number in
Text("\(answers[number])")
}
}
.onAppear {
for _ in (0..<4) {
answers.append(Int.random(in: 1...10)) /// you probably meant `answers.append`, not `answerArray.append`
}
}
}
}
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.