Home > Software engineering >  SwiftUI - get user defaults in a view model in combination with a Json file
SwiftUI - get user defaults in a view model in combination with a Json file

Time:08-31

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

enter image description here

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
}
  • Related