Using ForEach
, I want to create individual Toggles for each row. Right now, the @State
binding toggles all of the items at the same time, and I can't figure out how to separate them.
In the code below, I put a hard-coded array, but it really comes from an ever-changing .json file. Therefore, I need the ForEach
and the binding to be dynamic.
This post on hiding List
items and this post on problems with List
rows were helpful, but I couldn't make the binding work for my project. I'm on day 2 trying to figure this out, and none of what I've found online addresses this specific question.
Below is a small example of my code that reproduces my challenge. The dynamic data from the array comes from a .json file.
import SwiftUI
struct GreekWords: Codable, Hashable {
var greekWordArray = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"]
// The array data comes from a dynamic .json file
}
struct ContentView: View {
var greekWords: GreekWords
@State private var wordToggle = false
var body: some View {
VStack(spacing: 0) {
ForEach(greekWords.greekWordArray, id: \.self) { word in
Toggle(word, isOn: $wordToggle)
}
}
.padding(.horizontal)
}
}
I expect this is a simple solution, so I thank you in advance for any help. Also, I would appreciate any direction you might point me to better learn SwiftUI. I've tried all the Apple tutorials and books and the 100 days of SwiftUI on HackingWithSwift.
Cheers!
In your example code, all toggles are referencing to the same variable. So of course all toggles will always show the same state.
In the example implementation in the link you provided, it is not just an array of strings, it is an array of objects, that also contain a bool variable to control that specific item by a toggle.
UPDATE (2):
Maybe the following approach is more what you expected. Sorry, that I didn't thought about it last night. But please keep in mind, the var for the toggle state is only available in that view, you can show the status in that view, but you can't really work with it. If you want to (re-)use that information, I'd rather take the alternative from last night (see below).
//
// GreekWordTest.swift
// GreekWordTest
//
// Created by Sebastian on 15.08.22.
//
import SwiftUI
struct GreekWords: Codable, Hashable {
var greekWordArray = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"]
// The array data comes from a dynamic .json file
}
struct ContentView: View {
var greekWords: GreekWords
var body: some View {
VStack(spacing: 0) {
ForEach(greekWords.greekWordArray, id: \.self) { word in
GreekWordToggleView(greekWord: word)
.padding()
}
}
.padding(.horizontal)
}
}
struct GreekWordToggleView: View {
var greekWord: String
@State private var wordToggle = false
var body: some View {
VStack(spacing: 0) {
Toggle(greekWord, isOn: $wordToggle)
}
.padding(.horizontal)
}
}
And here the screenshot:
ALTERNATIVE:
The approach from last night
//
// GreekWordTest.swift
// GreekWordTest
//
// Created by Sebastian on 14.08.22.
//
import SwiftUI
struct ContentView: View {
@StateObject var greekWordsViewModel = GreekWordsViewModel()
var body: some View {
VStack() {
GreekWordView(greekWordsViewModel: greekWordsViewModel)
}
// For this test I am fetching the data once in the beginning when ContentView apears the first time, later I also added a button to fetch it again, it'll overwrite the existing data. You can also add a logic just to update it, that is up to you and your needs.
.onAppear(){
greekWordsViewModel.fetchData()
}
}
}
struct GreekWordView: View {
@ObservedObject var greekWordsViewModel: GreekWordsViewModel
var body: some View {
VStack(){
ForEach(greekWordsViewModel.greekWordArray.indices, id: \.self){ id in
Toggle(greekWordsViewModel.greekWordArray[id].name, isOn: $greekWordsViewModel.greekWordArray[id].isOn)
.padding()
}
// Here is the extra button to (re-)fetch the data from the json.
Button(action: {
greekWordsViewModel.fetchData()
}) {
Text("Fetch Data")
}
.padding()
}
}
}
struct GreekWord: Identifiable, Hashable {
var id: String = UUID().uuidString
var name: String
var isOn: Bool
}
class GreekWordsViewModel: ObservableObject {
@Published var greekWordArray: [GreekWord] = []
func fetchData(){
// As mentioned above, in his example I empty the array on each new loading event. You can also implement a logic to just update the data.
greekWordArray = []
let greekWords: [String] = load("greekWordsData.json")
for greekWord in greekWords {
greekWordArray.append(GreekWord(name: greekWord, isOn: false))
}
}
}
For decoding the json, I used the following:
//
// ModelData.swift
// SwiftTest
//
// Created by Sebastian Fox on 14.08.22.
//
import Foundation
// This function is used to decode a file with a json. I guess you already created something that is decoding a json according to your need, of course you can still use it.
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
And finally for testing, I used a very simple greekWordsData.json file that just contains:
["Alpha", "Beta", "Delta", "Gamma", "Epsilon", "Zeta"]
Here a screenshot:
Best, Sebastian