SwiftUI 列出单个可选项

Posted

技术标签:

【中文标题】SwiftUI 列出单个可选项【英文标题】:SwiftUI List single selectable item 【发布时间】:2021-02-16 05:49:50 【问题描述】:

我正在尝试创建一个List 并一次只允许选择一项。我将如何在ForEach 循环中这样做?我可以选择多个项目就好了,但最终目标是在List 的选定项目中只有一个复选标记。它甚至可能不是处理我正在尝试的正确方法。

struct ContentView: View 

    var body: some View 
        NavigationView 
            List((1 ..< 4).indices, id: \.self)  index in
                CheckmarkView(index: index)
                    .padding(.all, 3)
            
            .listStyle(PlainListStyle())
            .navigationBarTitleDisplayMode(.inline)
            //.environment(\.editMode, .constant(.active))
        
    


struct CheckmarkView: View 
    let index: Int
    @State var check: Bool = false
    
    var body: some View 
        Button(action: 
            check.toggle()
        ) 
            HStack 
                Image("Image-\(index)")
                    .resizable()
                    .frame(width: 70, height: 70)
                    .cornerRadius(13.5)
                Text("Example-\(index)")
                Spacer()
                if check 
                    Image(systemName: "checkmark")
                        .resizable()
                        .frame(width: 12, height: 12)
                
            
        
    

【问题讨论】:

这能回答你的问题吗? How to make List with single selection with SwiftUI 5 【参考方案1】:

您需要一些东西来存储所有状态,而不是在每个复选标记视图中存储它,因为需要一次只检查一件事。我做了一个小例子,其中逻辑在ObservableObject 中处理,并通过处理检查/取消检查状态的自定义Binding 传递给复选标记视图:

struct CheckmarkModel 
    var id = UUID()
    var state = false


class StateManager : ObservableObject 
    @Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
    
    func singularBinding(forIndex index: Int) -> Binding<Bool> 
        Binding<Bool>  () -> Bool in
            self.checkmarks[index].state
         set:  (newValue) in
            self.checkmarks = self.checkmarks.enumerated().map  itemIndex, item in
                var itemCopy = item
                if index == itemIndex 
                    itemCopy.state = newValue
                 else 
                    //not the same index
                    if newValue 
                        itemCopy.state = false
                    
                
                return itemCopy
            
        
    


struct ContentView: View 
    @ObservedObject var state = StateManager()
    
    var body: some View 
        NavigationView 
            List(Array(state.checkmarks.enumerated()), id: \.1.id)  (index, item) in //<-- here
                CheckmarkView(index: index + 1, check: state.singularBinding(forIndex: index))
                    .padding(.all, 3)
            
            .listStyle(PlainListStyle())
            .navigationBarTitleDisplayMode(.inline)
        
    


struct CheckmarkView: View 
    let index: Int
    @Binding var check: Bool //<-- Here
    
    var body: some View 
        Button(action: 
            check.toggle()
        ) 
            HStack 
                Image("Image-\(index)")
                    .resizable()
                    .frame(width: 70, height: 70)
                    .cornerRadius(13.5)
                Text("Example-\(index)")
                Spacer()
                if check 
                    Image(systemName: "checkmark")
                        .resizable()
                        .frame(width: 12, height: 12)
                
            
        
    


发生了什么:

    有一个 CheckmarkModel,每个复选框都有一个 ID,以及该框的状态 StateManager 保留了这些模型的数组。它还为数组的每个索引提供了自定义绑定。对于getter,它只返回该索引处模型的状态。对于setter,它会创建一个复选框数组的新副本。任何时候设置一个复选框,它都会取消选中所有其他框。我还保留了您最初不允许检查的行为 List 现在获得了state.checkmarks 的枚举——使用enumerated 可以让我保持您之前能够将索引号传递给复选框视图的行为 在 ForEach 内部,创建之前的自定义绑定并将其传递给子视图 在子视图中,不使用@State,而是使用@Binding(这是自定义Binding传递给的)

【讨论】:

很好的解释和很好的例子,特别是对于刚刚学习的人。【参考方案2】:

我们可以从 Swift 5.5 的绑定集合中受益。

import SwiftUI

struct CheckmarkModel: Identifiable, Hashable 
    var id = UUID()
    var state = false


class StateManager : ObservableObject 
    @Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]


struct SingleSelectionList<Content: View>: View 
    
    @Binding var items: [CheckmarkModel]
    @Binding var selectedItem: CheckmarkModel?
    var rowContent: (CheckmarkModel) -> Content
    @State var previouslySelectedItemNdx: Int?
    
    var body: some View 
        List(Array($items.enumerated()), id: \.1.id)  (ndx, $item) in
            rowContent(item)
                .modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
                .contentShape(Rectangle())
                .onTapGesture 
                    if let prevIndex = previouslySelectedItemNdx 
                        items[prevIndex].state = false
                    
                    self.selectedItem = item
                    item.state = true
                    previouslySelectedItemNdx = ndx
                
        
    


struct CheckmarkModifier: ViewModifier 
    var checked: Bool = false
    func body(content: Content) -> some View 
        Group 
            if checked 
                ZStack(alignment: .trailing) 
                    content
                    Image(systemName: "checkmark")
                        .resizable()
                        .frame(width: 20, height: 20)
                        .foregroundColor(.green)
                        .shadow(radius: 1)
                
             else 
                content
            
        
    


struct ContentView: View 
    
    @ObservedObject var state = StateManager()
    @State private var selectedItem: CheckmarkModel?
    
    
    var body: some View 
        VStack 
            Text("Selected Item: \(selectedItem?.id.description ?? "Select one")")
            Divider()
            SingleSelectionList(items: $state.checkmarks, selectedItem: $selectedItem)  item in
                HStack 
                    Text(item.id.description + " " + item.state.description)
                    Spacer()
                
            
        
    
    

稍微简化的版本

struct ContentView: View 

    @ObservedObject var state = StateManager()
    @State private var selection: CheckmarkModel.ID?

    var body: some View 
        List 
            ForEach($state.checkmarks)  $item in
                SelectionCell(item: $item, selectedItem: $selection)
                    .onTapGesture 
                        if let ndx = state.checkmarks.firstIndex(where:  $0.id == selection) 
                        state.checkmarks[ndx].state = false
                        
                        selection = item.id
                        item.state = true
                    
            
        
        .listStyle(.plain)
    


struct SelectionCell: View 

    @Binding var item: CheckmarkModel
    @Binding var selectedItem: CheckmarkModel.ID?

    var body: some View 
        HStack 
            Text(item.id.description + " " + item.state.description)
            Spacer()
            if item.id == selectedItem 
                Image(systemName: "checkmark")
                    .foregroundColor(.accentColor)
            
        
    

使用内部List 的选定标记和选择的版本:

import SwiftUI

struct CheckmarkModel: Identifiable, Hashable 
    var name: String
    var state: Bool = false
    var id = UUID()


class StateManager : ObservableObject 
    @Published var checkmarks = [CheckmarkModel(name: "Name1"), CheckmarkModel(name: "Name2"), CheckmarkModel(name: "Name3"), CheckmarkModel(name: "Name4")]


struct ContentView: View 
    
    @ObservedObject var state = StateManager()
    @State private var selection: CheckmarkModel.ID?
    @State private var selectedItems = [CheckmarkModel]()
    
    var body: some View 
        
        VStack 
            Text("Items")
            List($state.checkmarks, selection: $selection)  $item in
                Text(item.name + " " + item.state.description)
            
            .onChange(of: selection)  s in
                for index in state.checkmarks.indices 
                    if state.checkmarks[index].state == true 
                        state.checkmarks[index].state = false
                    
                
                selectedItems = []
                
                if let ndx = state.checkmarks.firstIndex(where:  $0.id == selection) 
                    state.checkmarks[ndx].state = true
                    selectedItems = [state.checkmarks[ndx]]
                    print(selectedItems)
                
            
            .environment(\.editMode, .constant(.active))
            
            Divider()
            List(selectedItems) 
                Text($0.name + " " + $0.state.description)
            
        
        Text("\(selectedItems.count) selections")
    

【讨论】:

【参考方案3】:
List 
    ForEach(0 ..< RemindTimeType.allCases.count) 
        index in CheckmarkView(title:getListTitle(index), index: index, markIndex: $markIndex)
            .padding(.all, 3)
    .listRowBackground(Color.clear)


struct CheckmarkView: View 
    let title: String
    let index: Int
    @Binding var markIndex: Int
    
    var body: some View 
        Button(action: 
            markIndex = index
        ) 
            HStack 
                Text(title)
                    .foregroundColor(Color.white)
                    .font(.custom(FontEnum.Regular.fontName, size: 14))
                Spacer()
                if index == markIndex 
                    Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(Color(hex: 0xe6c27c))
                
            
        
    

【讨论】:

请不要只发布代码作为答案,还要解释您的代码的作用以及它如何解决问题的问题。带有解释的答案通常更有帮助、质量更好,并且更有可能吸引投票。

以上是关于SwiftUI 列出单个可选项的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI - 列出嵌套数组中的元素

SwiftUI:为 .toolbar ToolbarItem 设置单独的颜色?

SwiftUI - 从嵌套数组中列出对象

尝试使用 SwiftUI 制作可滑动的图像视图时,索引意外更改

在 SwiftUI 中使用 FileManager 列出文档

单个 Slider 的 Swiftui 多重绑定