Please help, I have a view model, local json and I would like to use user defaults in this that view model so I can load and then modify it and to be able clear the selected items with one button created in a view. In view I used @AppStorage
and it works as expected. Now I'm trying to create view model also using app storage or user defaults instead of heaving it in a view.
Model:
import Foundation
struct CountriesSection: Codable, Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var items: [Country]
}
struct Country: Codable, Equatable, Identifiable,Hashable {
var id: UUID = UUID()
var name: String
var isOn: Bool = false
}
View model:
import Foundation
class ItemSelectionViewModel: ObservableObject {
@Published var itemSections: [CountriesSection] = []
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "countries", withExtension: "json")
else {
print("Json file not found")
return
}
do {
let data = try Data(contentsOf: url)
let sections = try JSONDecoder().decode([CountriesSection].self, from: data)
//UserDefaults.standard.set(sections, forKey: "saved1")
self.itemSections = sections
} catch {
print("failed loading or decoding with error: ", error)
}
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in itemSections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i filteredItems.count
}
return i
}
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
ContentView:
import SwiftUI
struct ContentView: View {
//@StateObject var viewModel = ItemSelectionViewModel()
@EnvironmentObject var viewModel: ItemSelectionViewModel
var body: some View {
VStack() {
CheckItemView()
Total().multilineTextAlignment(.center)
Spacer()
Button("Clear seclections"){
viewModel.itemSections = []
}
}
}
}
Total view:
import SwiftUI
struct Total: View {
@EnvironmentObject var viewModel: ItemSelectionViewModel
var body: some View {
VStack(alignment: .center){
Text("Number of checked items: \($viewModel.getSelectedItemsCount)")
.padding()
}}
}
struct Total_Previews: PreviewProvider {
static var previews: some View {
Total()
}
}
CheckItemView
import SwiftUI
struct CheckItemView: View {
@EnvironmentObject var viewModel: ItemSelectionViewModel
var body: some View {
NavigationView{
VStack() {
List(){
ForEach(viewModel.itemSections.indices, id: \.self){ id in
NavigationLink(destination: ItemSectionDetailedView(
items: $viewModel.itemSections[id].items)) {
Text(viewModel.itemSections[id].name)
}
.padding()
}
}
}
.listStyle(.insetGrouped)
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ItemSectionDetailedView view
import SwiftUI
struct ItemSectionDetailedView: View {
@Binding var items: [Country]
var body: some View {
VStack{
ScrollView() {
ForEach(items.indices, id: \.self){ id in
HStack{
Toggle(items[id].name, isOn: $items[id].isOn).toggleStyle(CheckToggleStyle()).tint(.mint)
.padding()
Spacer()
}
}
}
.frame(maxWidth: .infinity,alignment: .topLeading)
}
}
}
struct CheckToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
Button {
configuration.isOn.toggle()
} label: {
Label {
configuration.label
} icon: {
Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square")
.foregroundColor(configuration.isOn ? .accentColor : .secondary)
.accessibility(label: Text(configuration.isOn ? "Checked" : "Unchecked"))
.imageScale(.large)
}
}
.buttonStyle(PlainButtonStyle())
}
}
Main view:
import SwiftUI
@main
struct MyApp: App {
@StateObject var varModel = ItemSelectionViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(varModel)
}
}
}
CodePudding user response:
You don´t have to load your data from JSON all the time. Load it only if the itemsSections
collection is empty. Use @AppStorage
in the Viewmodel the same way you use it in a view:
class ItemSelectionViewModel: ObservableObject {
//Change this to @AppStorage
@AppStorage("saved1") var itemSections: [CountriesSection] = []
init(){
//load data only if there are no items in your array
if itemSections.isEmpty{
loadData()
}
}
func loadData() {
guard let url = Bundle.main.url(forResource: "countries", withExtension: "json")
else {
print("Json file not found")
return
}
do {
let data = try Data(contentsOf: url)
let sections = try JSONDecoder().decode([CountriesSection].self, from: data)
self.itemSections = sections
} catch {
print("failed loading or decoding with error: ", error)
}
}
func getSelectedItemsCount() -> Int{
var i: Int = 0
for itemSection in itemSections {
let filteredItems = itemSection.items.filter { item in
return item.isOn
}
i = i filteredItems.count
}
return i
}
}
Edit:
Regarding your comment on clearing the Userdefaults
. You don´t need this anymore. You can also delete the extension if it is not needed anywhere else. To reset the collection just do:
Button("Clear seclections"){
viewmodel.itemsSections = []
}
as you will find out this won´t work. Because your Viewmodel is only in your CheckItemView
. But it is in the wrong place anyway. Pull it up to ContentView
and pass it on either through the .environment(...
or as var. Your Total
view should also get the same Viewmodel
instance instead of creating a new one.
Edit2:
I was deriving my solution from the code you provided. In your example you are clearing the Userdefaults
and as there are no items left there is nothing presented. Solution should be simple:
Button("Clear seclections"){
viewmodel.itemsSections = []
viewmodel.loadData() // add this
}