SwiftUI:切换 ForEach 的各个项目

问题描述 投票:0回答:2

使用

ForEach
,我想为每一行创建单独的切换。现在,
@State
绑定同时切换所有项目,我不知道如何将它们分开。

在下面的代码中,我放置了一个硬编码数组,但它实际上来自一个不断变化的 .json 文件。因此,我需要

ForEach
和动态绑定。

这篇关于隐藏 List

 项目的文章
和关于 List 行问题的
这篇文章
很有帮助,但我无法为我的项目进行绑定。我在第二天试图解决这个问题,但我在网上找到的所有内容都没有解决这个具体问题。

下面是我的代码的一个小示例,它重现了我的挑战。数组中的动态数据来自 .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)
    }
}

我希望这是一个简单的解决方案,所以我提前感谢您的帮助。另外,如果您能为我更好地学习 SwiftUI 提供任何指导,我将不胜感激。我已经尝试过所有 Apple 教程和书籍以及 HackingWithSwift 上的 SwiftUI 100 天。

干杯!

arrays swiftui foreach toggle
2个回答
0
投票

在您的示例代码中,所有切换都引用同一变量。因此,当然所有切换将始终显示相同的状态。

在您提供的链接中的示例实现中,它不仅仅是一个字符串数组,它是一个对象数组,还包含一个 bool 变量来通过切换控制该特定项目。

更新(2):

也许下面的方法更符合您的期望。抱歉,昨晚我没有考虑到这一点。但请记住,切换状态的 var 仅在该视图中可用,您可以在该视图中显示状态,但无法真正使用它。如果您想(重新)使用该信息,我宁愿采用昨晚的替代方案(见下文)。

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

这里是截图:

替代方案:

昨晚的做法

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

为了解码 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)")
    }
}

最后为了测试,我使用了一个非常简单的 greekWordsData.json 文件,其中仅包含:

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

这里是截图:

最好的,塞巴斯蒂安


0
投票

我的方法是基于真正的开关创建“假开关”,作为开关使用:D

你需要:

  1. 使用“checked”属性在对象周围创建包装器
  2. 用数据生成数组
fileprivate struct QuestToggleModel {
    let item: String
    var checked: Bool = true
    
    init(_ item: String) {
        self.item = item
    }
}

let quests: [String] = [
    "Clean teeth",
    "Dantist visit",
    "Clean my apartment"
]
  1. 将其应用到您的视图中:
struct MyData: View {
    @State private var questModels = quests.map{ QuestToggleModel($0) }
    
    var body: some View {
        VStack {
           ForEach(questModels.indices) { idx in
               HStack {
                  Toggle("", isOn: .constant(self.questModels[idx].checked))
                
                  Text(self.questModels[idx].item)
               }
               .overlay(Color.clickableAlpha)
               .onTapGesture { self.questModels[idx].checked.toggle() }
           }
        }
    }
}

结果:

clickableAlpha 这里是:

public extension Color {
    init(rgbaHex: UInt32) {
        self.init(
            red:      Double((rgbaHex >> 24) & 0xFF) / 256.0,
            green:    Double((rgbaHex >> 16) & 0xFF) / 256.0,
            blue:     Double((rgbaHex >> 8) & 0xFF) / 256.0,
            opacity:  Double(rgbaHex & 0xFF) / 256.0
        )
    }

    static var clickableAlpha: Color { get { return Color(rgbaHex: 0x01010101) } }
}
© www.soinside.com 2019 - 2024. All rights reserved.