Swift View launching before data build - arrays

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

Related

How to bind a list of textfields that edit a variable within an a core data array?

Swift ui requires a Binding<String> to link to the value you are updating in a text field. Much like the native iPhone Reminders app, I am looking to permit inline editing a list that will persist.
The attached code works only but gives the same name for each item due to them all being bound to the same variable. How can I bind this to the [FruitEntity] array?
class CoreDataViewModel: ObservableObject {
//static let instance = CoreDataViewModel()
let container: NSPersistentContainer
let context: NSManagedObjectContext
#Published var savedEntities: [FruitEntity] = []
}
struct Screen: View {
#StateObject var vm = CoreDataViewModel()
var body: some View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: entity.name) { text in
entity.name = questionVariable
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}
}
}
You can just move the TextField to a separate view, with its own #State var for the field and another var for the entity.
Create a view like the following one:
struct ChangeName: View {
// Will change the entity
let entity: FruitEntity
// Will update the field
#State private var questionVariable = ""
var body: some View {
TextField("\(entity.name ?? "Workout Name...")", text: $questionVariable)
.onChange(of: questionVariable) { text in
entity.name = text
// Remember to save the persistent container/ managed-object-context
}
}
}
Call it in your main view:
struct Screen: View {
List{
ForEach(vm.savedEntities, id: \.self) {entity in
VStack{
HStack {
ChangeName(entity: entity)
}
}
.onDelete(perform: vm.deleteFruit)
.onMove(perform: moveItem)
}
}
}

Using an array within a view model

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.

make a button that appends structs to an array in swift

I am trying to make a macOS list/todo app with swift and want to add a button that adds appends a struct to an array, but it shows this error: "Cannot use mutating member on immutable value: 'self' is immutable"
here is my code
import SwiftUI
struct Lists{
var title : String
var id = UUID()
}
struct SidebarView: View {
var list = [Lists(title: "one"),Lists(title: "two"),Lists(title: "three")]
var body: some View {
List{
Button(action:{
let item = Lists(title:"hello")
list.append(item)
}) {
Text("add")
}
ForEach(list, id : \.id ){ item in
NavigationLink(destination:DetailView(word:item.title)){
Text(item.title)
}
}
}
.listStyle(SidebarListStyle())
.frame(maxWidth:200)
}
}
struct SidebarView_Previews: PreviewProvider {
static var previews: some View {
SidebarView()
}
}
the code that says list.append is the error
You need to declare list as #State variable.
#State var list = [Lists(title: "one"),Lists(title: "two"),Lists(title: "three")]
Then append new item to list:
Button(action:{
let item = Lists(title:"hello")
self.list.append(item)
}) {
Text("add")
}

Multidimensional lists in SwiftUI that pass data

I have this model:
struct Training: Identifiable {
let id = UUID()
let name: String
let workout: [Workout]?
}
and:
struct Workout: Identifiable {
let id = UUID()
let name: String
let exercices: [Exercice]?
}
and:
struct Exercice: Identifiable {
let id = UUID()
let name: String
}
The data for the models is coming from an environment object.
The app will launch with an empty list of trainings and you can add trainings within the UI. Each training has a navigtaionlink to a view to add workouts to each training and in the next step you can add exercices to each workout.
In my logic I create multidimensional arrays with the structs shown above.
The trainings view is easy:
struct TrainingsView: View {
#EnvironmentObject var appState: AppState
#State var showingDetail = false
var body: some View {
NavigationView {
VStack {
List {
ForEach (appState.trainings) { training in
NavigationLink(destination: WorkoutsView(training: training).environmentObject(self.appState)) {
Text(training.name)
}
}
.onDelete(perform: appState.removeTraining)
}
// Button to add trainings....
.navigationBarTitle(Text("Trainings").foregroundColor(Color.white))
}
}
}
}
The WorkoutsView is looking the same but I have an issue with listing the items of the parent training:
struct WorkoutsView: View {
// ...
var training: Training
var body: some View {
VStack {
List {
ForEach (appState.trainings(training).workouts) { workout in // I know the appState call is incorrect, but I don't know how to access is correctly.
NavigationLink(destination: ExercicesView(workout)) {
Text(workout.name)
}
}
}
// ...
}
}
}
I already tried:
List {
ForEach (0 ..< appState.trainings.count) {
NavigationLink(destination: WorkoutsView(training: $0).environmentObject(self.appState)) {
Text(appState.trainings[$0].name)
}
}
}
I could use appState.trainings[training].workouts in the WorkoutsView but I'm getting the error Contextual closure type '() -> Text' expects 0 arguments, but 1 was used in closure body on the NavigationLink line and don't know what to do.
Additional question: If this is close to the solution, I don't need the struct to conform to Identifiable?
You have 2 broad approaches here, depending on how you want to design your system.
1. Your child views know about the app state and can modify it directly. So, the parent needs to pass the indices/keys for the child to locate which data to modify:
struct TrainingsView: View {
#EnvironmentObject var appState: AppState
var body: some View {
NavigationView {
List(0..<appState.trainings.count) { i in
NavigationLink(destination: WorkoutsView(trainingIndex: i)) {
Text(self.appState.trainings[i].name)
}
}
}
}
}
struct WorkoutsView: View {
var trainingIdx: Int
#EnvironmentObject var appState: AppState
var body: some View {
VStack() {
TextField("training name: ", text: $appState.trainings[trainingIdx].name)
Button(action: {self.appState.trainings[trainingIdx].workouts.append(Workout(...))}) {
Text("Add workout")
}
}
}
}
2. Alternatively, you might say that you don't want your child views to know about the app's state - you just want them to modify some static struct that they don't own (but rather owned by their parent), then you should use use a #Binding.
The example below is conceptual to illustrate a point:
struct TrainingsView: View {
#State var trainingA = Training(...)
#State var trainingB = Training(...)
var body: some Body {
NavigationView {
List {
WorkoutsView(training: $trainingA)
WorkoutsView(training: $trainingB)
}
}
}
}
struct WorkoutsView: View {
#Binding var training: Training
var body: some View {
VStack() {
TextField("training name: ", text: $training.name)
Button(action: { self.training.workouts.append(Workout(...)) }) {
Text("Add workout")
}
}
}
}

How do I make the Observable Object update the list?

So I know my items are being added to the 'vitallist'(through printing the list in the terminal), but I am not seeing them appear on list view. I think it has something to do with the 'ObservedObject' not being linked correctly. Any suggestions?
struct Vital: Identifiable {
let id = UUID()
var name: String
}
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
}
struct Row: View {
var vital: Vital
#State var completed:Bool = false
var body: some View {
HStack{
Image(systemName: completed ? "checkmark.circle.fill" : "circle").onTapGesture {
self.completed.toggle()
}
Text(vital.name)
}
}
}
struct Lists: View {
#ObservedObject var vitallist = VitalList()
var body: some View {
NavigationView{
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
I also had same problem.
I am not sure why, but it works that creating a new element in the array, not changing the element itself. I confirmed directly updating works only in data, but not for binding UI.
In my code, element change in TobuyData class.
class Tobuy: Identifiable {
let id = UUID()
var thing: String
var isDone = false
init(_ thing: String, isDone: Bool = false) {
self.thing = thing
self.isDone = isDone
}
}
class TobuyData: ObservableObject {
#Published var tobuys: [Tobuy]
init() {
self.tobuys = [
Tobuy("banana"),
Tobuy("bread"),
Tobuy("pencil"),
]
}
func toggleDone(_ tobuy: Tobuy) {
if let j = self.tobuys.firstIndex(where: { $0.id == tobuy.id }) {
self.tobuys[j] = Tobuy(self.tobuys[j].thing, isDone: !self.tobuys[j].isDone)
// self.tobuys[j].isDone.toggle() // this works only in data, but not for binding UI
}
}
}
In View
struct ContentView: View {
#EnvironmentObject var tobuyData: TobuyData
var body: some View {
List {
ForEach(tobuyData.tobuys) { tobuy in
Text(tobuy.thing)
.strikethrough(tobuy.isDone)
.onTapGesture { self.tobuyData.toggleDone(tobuy) }
...
p.s.
Changing Tobuy Class to Struct made direct element updating work, the comment out part above. This referenced to Apple's official tutorial: "Handling User Input"
change
#ObservedObject var vitallist = VitalList()
to
#EnvironmentObject var vitallist = VitalList()
The code seems fine. I added a simple add method to VitalList
class VitalList:ObservableObject {
#Published var vitallist = [Vital]()
func addVital(){
self.vitallist.append(Vital(name: UUID().description))
}
}
And a Button to the body
var body: some View {
NavigationView{
VStack{
Button(action: {self.vitallist.addVital()}, label: {Text("add-vital")})
List{
Section(header: Text("Vital")){
ForEach(vitallist.vitallist){ item in
Row(vital: item)
}
}
}
}
}
}
The list updates as expected. check your code that adds your items to
#Published var vitallist = [Vital]()
Are you using the same instance of VitalList? A singleton might help.
https://developer.apple.com/documentation/swift/cocoa_design_patterns/managing_a_shared_resource_using_a_singleton

Resources