fdvfarzin
fdvfarzin

Reputation: 1295

Why do I lose Data here?(SwiftUI)

I have two pages in my app TodayPage and CalendarList page. I use EnvironmentObject wrapper to pass data between these two pages. When TodayPage appears on onAppear modifier I call a function to generate days of calendar for me till now everything works fine when I add text to the list of TodayPage then go to the calendarList page and come back again to TodayPage all of the text that I addd to list are gone.I find out I can avoid lost of data by adding simple if to onAppear but I'm not sure this solution is right.

I have to upload lots of code ,Thanks for your help ( DataModel ) :

import SwiftUI
import Foundation
import Combine

struct Day : Identifiable {

var id = UUID()
var name : String
var date : String
var month : String
var List : [Text1?]

}

struct Text1 : Identifiable , Hashable{

var id = UUID()
var name: String
var color: Color

}

class AppState : ObservableObject {

@Published var dataLoaded = false

@Published var allDays : [Day] = [.init(name : "",date: "",month: "",List : [])]
    
func getDays(number: Int) -> [Day] {
    let today = Date()
    let formatter = DateFormatter()
    return (0..<number).map { index -> Day in
        let date = Calendar.current.date(byAdding: .day, value: index, to: today) ?? Date()
        return Day(name: date.dayOfWeek(withFormatter: formatter) ?? "", date: "\(Calendar.current.component(.day, from: date))", month: date.nameOfMonth(withFormatter: formatter) ?? "", List: [])
    }
    
}


}

extension Date {
    func dayOfWeek(withFormatter dateFormatter: DateFormatter) -> String? {
        dateFormatter.dateFormat = "EEEE"
        return dateFormatter.string(from: self).capitalized
    }

func nameOfMonth(withFormatter dateFormatter: DateFormatter) -> String? {
    dateFormatter.dateFormat = "LLLL"
    return dateFormatter.string(from: self).capitalized
}
}

class AddListViewViewModel : ObservableObject {
    @Published var textItemsToAdd : [Text1] = [.init(name: "", color: .clear)] //start with one empty item


func saveToAppState(appState: AppState) {
    
appState.allDays[0].List.append(contentsOf: textItemsToAdd.filter { 

!$0.name.isEmpty })
}



func bindingForId(id: UUID) -> Binding<String> {
    .init { () -> String in
        self.textItemsToAdd.first(where: { $0.id == id })?.name ?? ""
    } set: { (newValue) in
        self.textItemsToAdd = self.textItemsToAdd.map {
            guard $0.id == id else {
                return $0
            }
            return .init(id: id, name: newValue, color: .clear)
        }
    }
}
}

List view :

struct ListView: View {


@State private var showAddListView = false
@EnvironmentObject var appState : AppState
@Binding var dayList : [Text1?]
var title : String

var body: some View {
    NavigationView {
        VStack {
            ZStack {
                List(dayList,  id : \.self){ text in
                    Text(text?.name ?? "")
                    
                }
                if showAddListView {
                    AddListView(showAddListView: $showAddListView)
                        .offset(y:-100)
                }
            }
        }
        .navigationTitle(title)
        .navigationBarItems(trailing:
                                Button(action: {showAddListView = true}) {
                                    Image(systemName: "plus")
                                        .font(.title2)
                                }
        )
    }
    
}
}

pop up menu View(for adding text into the list)

struct AddListView: View {

@Binding var showAddListView : Bool
@EnvironmentObject var appState : AppState
@StateObject private var viewModel = AddListViewViewModel()

var body: some View {
    ZStack {
        Title(addItem: { viewModel.textItemsToAdd.append(.init(name: "", color: .clear)) })
        VStack {
            ScrollView {
                ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
                    PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
                }
            }
        }
        .padding()
        .offset(y: 40)
        
        Buttons(showAddListView: $showAddListView, save: {
            viewModel.saveToAppState(appState: appState)
        })
        
    }
    .frame(width: 300, height: 200)
    .background(Color.white)
    .shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}

struct PreAddTextField: View {
@Binding var textInTextField : String

var body: some View {
    VStack {
        TextField("Enter text", text: $textInTextField)
    }
}
}

struct Buttons: View {
@Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
    VStack {
        HStack(spacing:100) {
            Button(action: {
                    showAddListView = false}) {
                Text("Cancel")
            }
            Button(action: {
                showAddListView = false
                save()
            }) {
                Text("Add")
            }
        }
    }
    .offset(y: 70)
}
}

struct Title: View {

var addItem : () -> Void 

var body: some View {
    VStack {
        HStack {
            Text("Add Text to list")
                .font(.title2)
            Spacer()
            Button(action: {
                addItem()
            }) {
                Image(systemName: "plus")
                    .font(.title2)
            }
        }
        .padding()
        Spacer()
    }
}
}

TodayPage View :

struct TodayPage: View {
@EnvironmentObject var appState : AppState

var body: some View {
    ListView(dayList: $appState.allDays[0].List, title: "Today")
        .onAppear {
        // To avoid data lost , we can use simple if below but I'm not sure it's a right solution
        //    if appState.dataLoaded == false {
               appState.allDays = appState.getDays(number: 365)
        //       appState.dataLoaded = true
        //    }
            
        }
}
}

CalendarListPage :

struct CalendarList: View {

@EnvironmentObject var appState : AppState

var body: some View {
    
        NavigationView {
            
            List {
                
                ForEach(appState.allDays.indices, id:\.self) { index in
                    NavigationLink(destination: ListView(appState: _appState, dayList: $appState.allDays[index].List, title: appState.allDays[index].name).navigationBarTitleDisplayMode(.inline) ) {
                        HStack(alignment: .top) {
                            RoundedRectangle(cornerRadius: 23)
                                .frame(width: 74, height: 74)
                                .foregroundColor(Color.blue)
                                .overlay(
                                    VStack {
                                        Text(appState.allDays[index].date)
                                            .font(.system(size: 35, weight: .regular))
                                            .foregroundColor(.white)
                                        Text(appState.allDays[index].month)
                                            .foregroundColor(.white)
                                    }
                                )
                                .padding(.trailing ,4)
                            
                            VStack(alignment: .leading, spacing: 5) {
                                Text(appState.allDays[index].name)
                                    .font(.system(size: 20, weight: .semibold))
                                
                            }
                        }
                        .padding(.vertical ,6)
                    }
                }
                
            }
            .navigationTitle("Calendar")
        }.onAppear {
          
        }
        .navigationViewStyle(StackNavigationViewStyle())
            
}
}

and finally TabBar :

struct TabBar: View {
var body: some View {
    let appState = AppState()
    
    TabView {
        TodayPage().tabItem {
            Image(systemName: "info.circle")
            Text("Today")
        }
        CalendarList().tabItem {
            Image(systemName: "square.fill.text.grid.1x2")
            Text("Calendar")
        }
    }
    .environmentObject(appState)
}
}

Upvotes: 0

Views: 356

Answers (1)

jnpdx
jnpdx

Reputation: 52575

Right now, because your let appState is inside the body of TabBar, it gets recreated every time TabBar is rendered. Instead, store it as a @StateObject (or @ObservedObject if you are pre iOS 14):

struct TabBar: View {
    @StateObject var appState = AppState()
    
    var body: some View {
        TabView {
            TodayPage().tabItem {
                Image(systemName: "info.circle")
                Text("Today")
            }
            CalendarList().tabItem {
                Image(systemName: "square.fill.text.grid.1x2")
                Text("Calendar")
            }
        }
        .onAppear {
            appState.allDays = appState.getDays(number: 365)
        }
        .environmentObject(appState)
    }
}

Then, remove your other onAppear on TodayPage

Upvotes: 4

Related Questions