简体   繁体   中英

SwiftUI: Toggle for individual items of ForEach

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM