简体   繁体   English

SwiftUI:切换 ForEach 的各个项目

[英]SwiftUI: Toggle for individual items of ForEach

Using ForEach , I want to create individual Toggles for each row.使用ForEach ,我想为每一行创建单独的切换。 Right now, the @State binding toggles all of the items at the same time, and I can't figure out how to separate them.现在, @State绑定同时切换所有项目,我不知道如何将它们分开。

In the code below, I put a hard-coded array, but it really comes from an ever-changing.json file.在下面的代码中,我放了一个硬编码的数组,但它确实来自一个不断变化的.json文件。 Therefore, I need the ForEach and the binding to be dynamic.因此,我需要ForEach和绑定是动态的。

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. 这篇关于隐藏List的文章和这篇关于List行问题的文章很有帮助,但我无法使绑定适用于我的项目。 I'm on day 2 trying to figure this out, and none of what I've found online addresses this specific question.我在第 2 天试图弄清楚这一点,我在网上找到的内容都没有解决这个具体问题。

Below is a small example of my code that reproduces my challenge.下面是我的代码的一个小例子,它重现了我的挑战。 The dynamic data from the array comes from a.json file.数组中的动态数据来自.json 文件。

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.另外,如果您能指出我更好地学习 SwiftUI 的任何方向,我将不胜感激。 I've tried all the Apple tutorials and books and the 100 days of SwiftUI on HackingWithSwift.我已经在 HackingWithSwift 上尝试了所有 Apple 教程和书籍以及 SwiftUI 的 100 天。

Cheers!干杯!

In your example code, all toggles are referencing to the same variable.在您的示例代码中,所有切换都引用同一个变量。 So of course all toggles will always show the same state.因此,当然所有切换都将始终显示相同的 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.在您提供的链接中的示例实现中,它不仅仅是一个字符串数组,它是一个对象数组,还包含一个 bool 变量来通过切换控制该特定项目。

UPDATE (2):更新(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.但请记住,切换 state 的 var 仅在该视图中可用,您可以在该视图中显示状态,但不能真正使用它。 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:为了解码 json,我使用了以下内容:

//
//  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:最后为了测试,我使用了一个非常简单的 greekWordsData.json 文件,它只包含:

["Alpha", "Beta", "Delta", "Gamma", "Epsilon", "Zeta"]

Here a screenshot:这是一个屏幕截图:

备选方案:每行单独切换

Best, Sebastian最好的,塞巴斯蒂安

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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