Using an array within a view model - arrays

I have an array within a view model. I cannot work out how to initialise it with dummy data so that I can access any element of the array later. The number of elements in the array changes each time the view loads so I cannot initialise it in the view model with "repeating".
I tried calling a function from .onAppear to append elements to the array, but .onAppear seems to run after my view has loaded so I get an error trying to access the array.
I'm obviously doing this wrong,
My view model is:
class DemoViewModel: ObservableObject {
#Published var array = [0]
}
My view is:
#ObservedObject private var demoViewModel = DemoViewModel()
// This changes every time the view is called from it's parent
var numberOfItemsInArray = 10
var body: some View {
List(0..<numberOfItemsInArray) { index in
Text("Hello, World! - \(index)")
// demoViewModel.array[index] = 1
// causes an error
}
}

You can pass the numberOfItemsInArray variable to DemoViewModel in init, so you can use Array(repeating:count:):
class DemoViewModel: ObservableObject {
#Published var array: [Int]
init(numberOfItemsInArray: Int) {
array = Array(repeating: 0, count: numberOfItemsInArray)
}
}
and initialise DemoViewModel like this:
struct ContentView: View {
#ObservedObject private var demoViewModel: DemoViewModel
init(numberOfItemsInArray: Int = 10) {
demoViewModel = DemoViewModel(numberOfItemsInArray: numberOfItemsInArray)
}
var body: some View {
List(0 ..< demoViewModel.array.count) { index in
Text("Hello, World! - \(index)")
}
}
}
Also, consider using #StateObject instead of #ObservedObject if you're using SwiftUI 2.

Related

SwiftUI: String property of an object is not displaying in Text, and how would you edit a string in an object?

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)
}
}
}

How can I update a TextField value and have its associated index in an array update as well?

I am having trouble updating values in an array that are displayed via a for each loop. These values are displayed in a text field.
The code in question
struct EditItemView: View {
let entity: RecipeEntity
#StateObject var viewModel = ViewModelEdit()
#State var imageToUpload: Data
#StateObject var vm = CoreDataRelationshipViewModel()
#Environment(\.presentationMode) var presentationMode
#State var stepInfo: String = ""
#State var textFieldCount: Int = 1
#State var stepNumber: [Int]
#State var recipeName: String = ""
#State var recipeArray: [RecipeStepModel]
var body: some View {
//some code between here and the problem code
List {
ForEach(recipeArray, id: \.id) { index in
HStack {
CustomTextField(item: index)
}
}.onDelete { (indexSet) in
recipeArray.remove(atOffsets: indexSet)
}
CustomTextField I am using that allows me to pass my Identifiable model into a foreach. This can be seen referenced in the for each above as CustomTextField(item: index)
struct CustomTextField : View {
#State var item : RecipeStepModel
var body : some View {
Text(String(item.stepNumber) + ".")
TextField("", text: $item.stepName)
}
}
Lastly, here is the model for the array referenced in the last #State variable declared #State var recipeArray: [RecipeStepModel]:
struct RecipeStepModel: Identifiable {
var id = UUID()
var stepName: String
var stepNumber: Int
}
The Question
How can I make a change to a textfield in a foreach loop and have its value in #State var recipeArray: [RecipeStepModel] be updated accordingly? For example, I edit the first TextField in the for each loop - how can I update index 0 in the array with its new value?
Additional Information
I posted a similar question the other day. I was able to get it working with .indices in the for each loop. The version of the question asked here is an attempt at restructuring my code using MVVM. Previous question and answer can be found here - I hope this helps!
You need to follow a architecture like MVVM. Make a model where you have your struct RecipeStepModel and then make a seperate ViewModel to add and delete recipes in your array. You should study the Combine framework in SwiftUI, it is used to get input from textfield and then store it in an array.
Check this tutorial from Peter Freise https://github.com/peterfriese/MakeItSo/tree/firebase-combine/final/MakeItSo/MakeItSo for reference.
I ended up solving the issue by converting #State var item: RecipeStepModel in my view model to #Binding var item: RecipeStepModel
struct CustomTextField : View {
#Binding var item : RecipeStepModel
var body : some View {
Text(String(item.stepNumber) + ".")
TextField("", text: $item.stepName)
}
Once this change was made, I had to alter the code in my ForEach to pass a binding to the CustomTextField view model. Additionally, I had to change ForEach(recipeArray, id: \.id) to ForEach(recipeArray.indices, id: \.self) :
List {
ForEach(recipeArray.indices, id: \.self) { index in
HStack {
CustomTextField(item: $recipeArray[index]) //<--change made here
}
}.onDelete { (indexSet) in
recipeArray.remove(atOffsets: indexSet)
}
I am now able to both successfully delete items from the list, and update items in the array simply by changing the value in the appropriate TextField

How to create subarrays without duplication Swift

I have a Meal structure in my SwiftUI project
struct Meal: Identifiable, Codable, Equatable {
var id = UUID().uuidString
var name: String
var time: String
var type: String
var recommendation: Bool
}
I also have the ContentViewModel class
class ContentViewModel: ObservableObject {
init() {
let allItemsInit = Bundle.main.decode([Meal].self, from: "menu.json")
self.allItems = allItemsInit
self.recomendationItems = allItemsInit.filter {$0.recommendation == true}
}
#Published var allItems: [Meal] = []
#Published var recomendationItems: [Meal] = []
}
Is it a correct approach that I just assign certain elements to the new array of recomendationItems, thereby duplicating them.
recomendationItems - just example, there will be a large number of such subarrays.
You don't need "subarrays" -- your View will get updated whenever allItems changes, so you can use other computed properties to provide the subarrays rather than making them actual separate containers.
For example:
class ContentViewModel: ObservableObject {
init() {
self.allItems = Bundle.main.decode([Meal].self, from: "menu.json")
}
#Published var allItems: [Meal] = []
var recommendedItems: [Meal] {
return allItems.filter {$0.recommendation == true}
}
}

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.

Swift View launching before data build

My Problem: I'm appending to an array that is used as parameter for my component. The component launches before anything has been appended. The array of the function gets returned before it got filled.
I have the following code:
struct ContentView: View {
//This gets data from a Firebase db
#ObservedObject var categories = getData()
func getCards() -> [AnimatedCard] {
var array = [AnimatedCard]()
for i in categories.datas{
array.append(AnimatedCard(cardContentImage: "test1", cardContentTitle: i.name, itemHeight: 300,itemWidth: 300))
}
return array
}
var body: some View {
CarouselView(views: self.getCards())
}
}
The Firebase part is working as displaying i.name in a List works.
CarouselView takes an Array of AnimatedCards. This is also working.
try this:
so it appears only if data is there.
struct ContentView: View {
//This gets data from a Firebase db
#ObservedObject var categories = getData()
func getCards() -> [AnimatedCard] {
var array = [AnimatedCard]()
for i in categories.datas{
array.append(AnimatedCard(cardContentImage: "test1", cardContentTitle: i.name, itemHeight: 300,itemWidth: 300))
}
return array
}
var body: some View {
Group {
if self.categories.datas.count > 0 {
CarouselView(views: self.getCards())
}
}
}
}

Resources