Home > Enterprise >  SwiftUI: Checkmarks disappear when changing from one view to another using NavigationLink
SwiftUI: Checkmarks disappear when changing from one view to another using NavigationLink

Time:08-25

I'm trying to make an app that is displaying lists with selections/checkmarks based on clicked NavigationLink. The problem I encountered is that my selections disappear when I go back to main view and then I go again inside the NavigationLink. I'm trying to save toggles value in UserDefaults but it's not working as expected. Below I'm pasting detailed and main content view.

Second view:

struct CheckView: View {
    
    @State var isChecked:Bool = false
    @EnvironmentObject var numofitems: NumOfItems
    
    var title:String
    var count: Int=0
    
    var body: some View {
        HStack{
            ScrollView {
                Toggle("\(title)", isOn: $isChecked)
                    .toggleStyle(CheckToggleStyle())
                    .tint(.mint)
                    .onChange(of: isChecked) { value in
                        
                        if isChecked {
                            numofitems.num  = 1
                            print(value)
                        } else{
                            numofitems.num -= 1
                        }
                        UserDefaults.standard.set(self.isChecked, forKey: "locationToggle")
                    }.onTapGesture {
                        
                    }
                    .onAppear {
                        self.isChecked = UserDefaults.standard.bool(forKey: "locationToggle")
                    }
                Spacer()
            }.frame(maxWidth: .infinity,alignment: .topLeading)
        }
    }
}

Main view:

struct CheckListView: View {
    
    @State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
    
    
    var body: some View {
        NavigationView{
            List{
                ForEach(menu){
                    section in
                    NavigationLink(section.name) {
                        VStack{
                            ScrollView{
                                ForEach(section.items) { item in
                                    CheckView( title: item.name)
                                }
                            }
                        }
                    }
                }
            }
        }.navigationBarHidden(true)
            .navigationViewStyle(StackNavigationViewStyle())
            .listStyle(GroupedListStyle())
            .navigationViewStyle(StackNavigationViewStyle())
    }
}

ItemsSection:

[
    {
        "id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
        "name": "Africa",
        "items": [
            {
                "id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
                "name": "Algeria"
            },
            {
                "id": "E124AA01-B66F-42D0-B09C-B248624AD228",
                "name": "Angola"
            }

Model:

struct ItemsSection: Codable, Identifiable, Hashable {
    var id: UUID = UUID()
    var name: String
    var items: [CountriesItem]
}

struct CountriesItem: Codable, Equatable, Identifiable,Hashable {
    var id: UUID = UUID()
    var name: String
}

CodePudding user response:

As already mentioned in the comment, I don'r see where you read back from UserDefaults, so whatever gets stored there, you don't read it. But even if so, each Toggle is using the same key, so you are overwriting the value.

Instead of using the @State var isChecked, which is used just locally, I'd create another struct item which gets the title from the json and which contains a boolean that gets initialized with false.

From what I understood, I assume a solution could look like the following code. Just a few things:

  1. I am not sure how your json looks like, so I am not loading from a json, I add ItemSections Objects with a title and a random number of items (actually just titles again) with a function.
  2. Instead of a print with the number of checked toggles, I added a text output on the UI. It shows you on first page the number of all checked toggles.
  3. Instead of using UserDefaults I used @AppStorage. To make that work you have to make Array conform to RawRepresentable you achieve that with the following code/extension (just add it once somewhere in your project)
  4. Maybe you should thing about a ViewModel (e.g. ItemSectionViewModel), to load the data from the json and provide it to the views as an @ObservableObject.

The code for the views:

//
//  CheckItem.swift
//  CheckItem
//
//  Created by Sebastian on 24.08.22.
//

import SwiftUI

struct ContentView: View {
    
    var body: some View {
        VStack() {
            CheckItemView()
        }
    }
}

struct CheckItemView: View {
    
    let testStringForTestData: String = "Check Item Title"
    
    @AppStorage("itemSections") var itemSections: [ItemSection] = []
    
    func addCheckItem(title: String, numberOfItems: Int) {
        var itemArray: [Item] = []
        for i in 0...numberOfItems {
            itemArray.append(Item(title: "item \(i)"))
        }
        itemSections.append(ItemSection(title: title, items: itemArray))
    }
    
    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
    }
    
    var body: some View {
        NavigationView{
            VStack() {
                List(){
                    ForEach(itemSections.indices, id: \.self){ id in
                        NavigationLink(destination: ItemSectionDetailedView(items: $itemSections[id].items)) {
                            Text(itemSections[id].title)
                        }
                        .padding()
                    }
                }
                Text("Number of checked items:  \(self.getSelectedItemsCount())")
                    .padding()
                
                Button(action: {
                    self.addCheckItem(title: testStringForTestData, numberOfItems: Int.random(in: 0..<4))
                }) {
                    Text("Add Item")
                }
                .padding()
            }
        }
    }
}

struct ItemSectionDetailedView: View {
    
    @Binding var items: [Item]
    
    var body: some View {
        ScrollView() {
            ForEach(items.indices, id: \.self){ id in
                Toggle(items[id].title, isOn: $items[id].isOn)
                    .padding()
            }
        }
    }
}

struct ItemSection: Identifiable, Hashable, Codable  {
    var id: String = UUID().uuidString
    var title: String
    var items: [Item]
}

struct Item: Identifiable, Hashable, Codable  {
    var id: String = UUID().uuidString
    var title: String
    var isOn: Bool = false
}

Here the adjustment to work with @AppStorage:

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

CodePudding user response:

As allready stated in the comment you have to relate the isChecked property to the CountryItem itself. To get this to work i have changed the model and added an isChecked property. You would need to add this to the JSON by hand if the JSON allread exists.

struct CheckView: View {
    
    @EnvironmentObject var numofitems: NumOfItems
    //use a binding here as we are going to manipulate the data coming from  the parent
    //and pass the complete item not only the name
    @Binding var item: CountriesItem
        
    var body: some View {
        HStack{
            ScrollView {
                //use the name and the binding to the item itself
                Toggle("\(item.name)", isOn: $item.isChecked)
                    .toggleStyle(.button)
                    .tint(.mint)
                // you now need the observe the isChecked inside of the item
                    .onChange(of: item.isChecked) { value in
                        if value {
                            numofitems.num  = 1
                            print(value)
                        } else{
                            numofitems.num -= 1
                        }
                    }.onTapGesture {
                        
                    }
                Spacer()
            }.frame(maxWidth: .infinity,alignment: .topLeading)
        }
    }
}

struct CheckListView: View {
    
    @State private var menu = Bundle.main.decode([ItemsSection].self, from: "items.json")
    
    var body: some View {
        NavigationView{
            List{
                ForEach($menu){ // from here on you have to pass a binding on to the decendent views
                    // mark the $ sign in front of the property name
                    $section in
                    NavigationLink(section.name) {
                        VStack{
                            ScrollView{
                                ForEach($section.items) { $item in
                                    //Pass the complete item to the CheckView not only the name
                                    CheckView(item: $item)
                                }
                            }
                        }
                    }
                }
            }
        }.navigationBarHidden(true)
            .navigationViewStyle(StackNavigationViewStyle())
            .listStyle(GroupedListStyle())
            .navigationViewStyle(StackNavigationViewStyle())
    }
}

Example JSON:

[
    {
        "id": "9DC6D7CB-B8E6-4654-BAFE-E89ED7B0AF94",
        "name": "Africa",
        "items": [
            {
                "id": "59B88932-EBDD-4CFE-AE8B-D47358856B93",
                "name": "Algeria",
                "isChecked": false
            },
            {
                "id": "E124AA01-B66F-42D0-B09C-B248624AD228",
                "name": "Angola",
                "isChecked": false
                
            }
        ]
    }
]

Remarks:

The aproach with JSON and storing this in the bundle will prevent you from persisting the isChecked property between App launches. Because you cannot write to the Bundle from within your App. The choice will persist as long as the App is active but will be back to default as soon as you either reinstall or force quit it.

  • Related