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:
- 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.
- 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.
- 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)
- 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.