【问题标题】:Restrict cell selection per section in UITableView swift在 UITableView swift 中限制每个部分的单元格选择
【发布时间】:2020-03-24 12:08:33
【问题描述】:

我想限制我的 tableview 的某些部分只允许选择 1 个单元格,到目前为止,我的所有单元格都可以被选择,无论它位于哪个部分

但是有一点点扭曲:我的部分是一个 [array] 并且根据不同的变量动态变化。

我的部分本身就是一个变量,所以我可以像这样以编程方式精确定位它们:

var section1 = [NSDictionary(objects: [NSLocalizedString("Alcohol use less than 24 hours", comment:""), 2],

EDIT2:有人指出我可以创建一个包含限制的变量

var restrictedSections: [[NSDictionary]: Bool] {return  [section1: true,section2: true,section3: true, section4: true, section4COPI: true, section5: true, section5COPI: true, section6: false, section7: false, section8: false] }

这不能被引用为 IndexPath,所以没有运气......但也许在正确的道路上?

tableView 中的一些代码(为 StackOverflow 读者简化):

// checkmarks when tapped

    func tableView(_ tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) {
        if let cell = tableView.cellForRow(at: indexPath) {
            if self.selectedIndexPaths.contains(indexPath) {

                cell.accessoryType = .none
                cell.backgroundColor = UIColor.clear
                self.selectedIndexPaths.remove(indexPath)

                if CrewMembersNumber == "1" {
                    if((indexPath).section == 0) {
                        self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section3score -= section3[(indexPath).row].object(forKey: "value") as! Int
                    }

                } else if CrewMembersNumber == "2" {
                    if((indexPath).section == 0) {
                        self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section2score -= section2[(indexPath).row].object(forKey: "value") as! Int
                    }
                } else if CrewMembersNumber == "3" {
                    if((indexPath).section == 0) {
                        self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section5score -= section5[(indexPath).row].object(forKey: "value") as! Int
                    }

                } else {
                    // if crewmemebernumber doest return 1-2 or 3
                    if((indexPath).section == 0) {
                        self.section1score -= section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section4score -= section4[(indexPath).row].object(forKey: "value") as! Int
                    }

                }

            } else {
                cell.accessoryType = .checkmark
                cell.backgroundColor = UIColor (red:236/255.0, green: 236/255, blue: 236/255, alpha: 1.0)
                self.selectedIndexPaths.add(indexPath)

                if CrewMembersNumber == "1" {
                    if((indexPath).section == 0) {
                        self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section3score += section3[(indexPath).row].object(forKey: "value") as! Int
                    }

                } else if CrewMembersNumber == "2" {
                    if((indexPath).section == 0) {
                        self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section2score += section2[(indexPath).row].object(forKey: "value") as! Int
                    }

                } else if CrewMembersNumber == "3" {
                    if((indexPath).section == 0) {
                        self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section5score += section5[(indexPath).row].object(forKey: "value") as! Int
                    }
                } else {
                    // if crewmemebernumber doest return 1-2 or 3
                    if((indexPath).section == 0) {
                        self.section1score += section1[(indexPath).row].object(forKey: "value") as! Int
                    } else if((indexPath).section == 1) {
                        self.section4score += section4[(indexPath).row].object(forKey: "value") as! Int
                    }
                }
            }
            self.updateToolbarAndLabel(self.totalScore)
            self.tableView.reloadData()
        }
    }


    func tableView(_ tableView: UITableView,cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

        cell.textLabel!.text = self.textForIndexPath(indexPath);
        cell.textLabel!.font = UIFont(name:"Avenir", size:19)
        cell.textLabel!.numberOfLines = 0
        cell.selectionStyle = UITableViewCellSelectionStyle.none;

        if(self.selectedIndexPaths.contains(indexPath)) {

            cell.accessoryType = .checkmark;
            cell.backgroundColor = UIColor (red:236/255.0, green: 236/255, blue: 236/255, alpha: 1.0)
        } else {
            cell.accessoryType = .none;
            cell.backgroundColor = UIColor.clear
        }
        return cell
 }

正如您所见,当我点击一个单元格时,它会添加一个值,并且该单元格会更改其背景颜色并添加一个复选标记,

我需要做的是,如果有一个单元格被选中,在某些只能选择 1 个的部分中,它需要检查该部分中是否有任何单元格被选中并取消选择它以支持新的用户点击。现在我完全不明白如何做到这一点

感谢您的帮助

【问题讨论】:

  • 你要保存选中行的部分,对吧
  • 不是真的,除非要限制的部分需要这样做
  • 嗯,我还是不明白第一个。
  • 好吧,我有多个部分,这些部分将根据用户在表格打开之前的选择出现(或不出现),简单地说是部分名称:section4; section5 和 section4COPI section5COPI 需要被“标记”为受限,我相信最好的方法是创建一个“var”,列出其中哪些实际上显示在 tableview 中以限制它。除非有其他方法可以做到这一点
  • 你的数据模型非常繁琐,效率低下。您应该使用自定义结构或类作为数据源数组(代表部分),其中包括代表行的项目。好处是您可以通过索引路径轻松引用任何对象。然后将isSelected 属性添加到反映选择状态的row 对象。这比使用额外的数组更有效且更可靠地维护。对于单选部分,获取当前选中的项目,将isSelected 设置为false 并在索引路径的项目中设置为true 并重新加载两行。

标签: ios swift uitableview swift3


【解决方案1】:

从根本上说,tableview 的最佳解决方案 (恕我直言) 是为您的表创建一个视图模型,根据需要操作数据,然后在表中反映该数据。然后,您尽一切可能让表格对数据更改做出反应,而不是尝试使用表格视图本身来反映数据或状态。

编辑:代码不再使用reloadData,而是使用performBatchUpdates 进行更优雅的演示。

我创建了一个项目,可以满足您的需求,您可以找到它here

视图数据包含在这里:

let pilots = "Pilots"
let crew = "Crew"
let passengers = "Passengers"

var sections: [String] = []
var multipleSelectionsAllowed: Set<String> = []

var members: [String: [String]] = [:]
var selectedMembers: Set<String> = []

前三个字符串常量允许我们对数据进行索引,并进行初始化:

sections = [pilots, crew, passengers] // initial ordering of sections
multipleSelectionsAllowed = [passengers]

数据是以编程方式创建的,请参阅附件项目或下面附件的完整代码。

你说这些部分可能会改变,所以sections是一个变量,我们稍后会改变它。

selectedMembers 包含typehash(即 Pilot、Crew 或 Passenger 及其姓名,因此它应该是唯一的。此数组将反映当前选择,作为数据而不是 indexPaths。

但是,我们需要 indexPaths 来反映 isSelected UI 更改:很好,我们将为此使用两个函数:

typealias KeyToValues = (section: String, name: String)

func sectionNameToHash(section: String, name: String) -> String {
    let hash = section + "|" + name
    return hash
}

func hashToSectionName(hash: String) -> KeyToValues {
    let array = hash.components(separatedBy: "|")
    assert(array.count == 2)
    return (array[0], array[1])
}

此外,我过去发现非常有用的方法是将更改单元格外观的代码放在一个地方,并在创建或更改单元格时调用它。随着用户界面的变化,您不会随着时间的推移而失去同步。

func updateCell(atIndexPath indexPath: IndexPath) {
    let cells = tableView.visibleCells
    for cell in cells {
        guard let path = tableView.indexPath(for: cell) else { continue }
        if path == indexPath {
            updateCell(cell, atIndexPath: indexPath)
        }
    }
}

func updateCell(_ cell: UITableViewCell, atIndexPath indexPath: IndexPath) {
    let section = sections[indexPath.section]
    guard let names = members[section] else { fatalError() }
    let name = names[indexPath.row]

    let hash = sectionNameToHash(section: section, name: name)
    let shouldBeSelected = selectedMembers.contains(hash)

    if shouldBeSelected {
        cell.accessoryType = .checkmark
        print("SELECTED", hash)
    } else {
        cell.accessoryType = .none
        print("DESELECTED", hash)
    }
}

您需要两者,因为在某些情况下您只有一个 indexPath,而不是单元格。

请注意,创建单元格时使用上述方法:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

    let section = sections[indexPath.section]
    guard let names = members[section] else { fatalError() }
    let name = names[indexPath.row]

    cell.textLabel?.text = name

    updateCell(cell, atIndexPath: indexPath)
    return cell
}

当 tableView 检测到选择时,您将首先查看现有的选定数据,并首先从数据中删除该选择,然后更新任何已选择单元格的 UI:

 override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    let section = sections[indexPath.section]
    guard let names = members[section] else { fatalError() }

    let canMultipleSelect = multipleSelectionsAllowed.contains(section)

    if !canMultipleSelect, let paths = tableView.indexPathsForSelectedRows {
        for path in paths {
            if path.section == indexPath.section {
                let name = names[path.row]
                let hash = sectionNameToHash(section: section, name: name)
                selectedMembers.remove(hash)
                updateCell(atIndexPath: path)
                tableView.deselectRow(at: path, animated: true)
            }
        }
    }
    return indexPath
}

然后,处理选择方法:

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let section = sections[indexPath.section]
    guard let names = members[section] else { fatalError() }
    let name = names[indexPath.row]
    let hash = sectionNameToHash(section: section, name: name)

    selectedMembers.insert(hash)
    print("SELECTED THE CELL AT", hash)
    updateCell(atIndexPath: indexPath)
}

瞧——一切如你所愿。但是,更好的是,您可以按照您所说的重新排列这些部分,并正确选择所有内容。示例代码在您选择第一行/列 5 秒后重新排列各个部分

if indexPath.section == 0 && indexPath.row == 0 {
    DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
        self.sections = [self.crew, self.pilots, self.passengers] // changed!
        tableView.reloadData()
        // all selections from the tableView are now gone
        // NOTE: none of the other data changes!
        for hash in self.selectedMembers {
            let value = self.hashToSectionName(hash: hash)
            guard
                let sectionNumber = self.sections.firstIndex(of: value.section),
                let names = self.members[value.section],
                let row = names.firstIndex(of: value.name)
            else { fatalError() }

            let indexPath = IndexPath(row: row, section: sectionNumber)
            self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
        }
    }
}

reload() 删除所有选择,因此上面的代码使用已知的选择成员来通知 tableView 选择列表,即使每个单元格不可见。

完整的课程

import UIKit

private final class MyCell: UITableViewCell {
    override var reuseIdentifier: String? { "cell" }
}

final class ViewController: UITableViewController {

    let pilots = "Pilots"
    let crew = "Crew"
    let passengers = "Passengers"

    var sections: [String] = []
    var multipleSelectionsAllowed: Set<String> = []

    var members: [String: [String]] = [:]
    var selectedMembers: Set<String> = []

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.register(MyCell.self, forCellReuseIdentifier: "cell")
        tableView.allowsMultipleSelection = true

        sections = [pilots, crew, passengers] // initial ordering of sections
        multipleSelectionsAllowed = [passengers]

        constructData()
    }

    private func constructData() {
        var array: [String] = []
        (1..<6).forEach { array.append("Pilot \($0)")}
        members[pilots] = array
        array.removeAll()

        (1..<20).forEach { array.append("Crew \($0)")}
        members[crew] = array
        array.removeAll()

        (1..<250).forEach { array.append("Passenger \($0)")}
        members[passengers] = array
    }

    // MARK: - Helpers -

    typealias KeyToValues = (section: String, name: String)

    func sectionNameToHash(section: String, name: String) -> String {
        let hash = section + "|" + name
        return hash
    }


    func hashToSectionName(hash: String) -> KeyToValues {
        let array = hash.components(separatedBy: "|")
        assert(array.count == 2)
        return (array[0], array[1])
    }

}

extension ViewController /*: UITableViewDataSource */ {

    override func numberOfSections(in: UITableView) -> Int {
        return sections.count
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let type = sections[section]
        let count = members[type]?.count ?? 0 // could use guard here too and crash if nil
        return count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)

        let section = sections[indexPath.section]
        guard let names = members[section] else { fatalError() }
        let name = names[indexPath.row]

        cell.textLabel?.text = name

        updateCell(cell, atIndexPath: indexPath)
        return cell
    }

    func updateCell(atIndexPath indexPath: IndexPath) {
        let cells = tableView.visibleCells
        for cell in cells {
            guard let path = tableView.indexPath(for: cell) else { continue }
            if path == indexPath {
                updateCell(cell, atIndexPath: indexPath)
            }
        }
    }

    func updateCell(_ cell: UITableViewCell, atIndexPath indexPath: IndexPath) {
        let section = sections[indexPath.section]
        guard let names = members[section] else { fatalError() }
        let name = names[indexPath.row]

        let hash = sectionNameToHash(section: section, name: name)
        let shouldBeSelected = selectedMembers.contains(hash)

        if shouldBeSelected {
            cell.accessoryType = .checkmark
            print("SELECTED", hash)
        } else {
            cell.accessoryType = .none
            print("DESELECTED", hash)
        }
    }

}

extension ViewController /* : UITableViewDelegate */ {

    override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        let section = sections[indexPath.section]
        guard let names = members[section] else { fatalError() }

        let canMultipleSelect = multipleSelectionsAllowed.contains(section)

        if !canMultipleSelect, let paths = tableView.indexPathsForSelectedRows {
            for path in paths {
                if path.section == indexPath.section {
                    let name = names[path.row]
                    let hash = sectionNameToHash(section: section, name: name)
                    selectedMembers.remove(hash)
                    updateCell(atIndexPath: path)
                    tableView.deselectRow(at: path, animated: true)
                }
            }
        }
        return indexPath
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let section = sections[indexPath.section]
        guard let names = members[section] else { fatalError() }
        let name = names[indexPath.row]
        let hash = sectionNameToHash(section: section, name: name)

        selectedMembers.insert(hash)
        print("SELECTED THE CELL AT", hash)
        updateCell(atIndexPath: indexPath)

    if indexPath.section == 0 && indexPath.row == 0 {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
            self.sections = [self.crew, self.pilots, self.passengers]
            tableView.reloadData()
            // all selections from the tableView are gone

            for hash in self.selectedMembers {
                let value = self.hashToSectionName(hash: hash)
                guard
                    let sectionNumber = self.sections.firstIndex(of: value.section),
                    let names = self.members[value.section],
                    let row = names.firstIndex(of: value.name)
                else { fatalError() }

                let indexPath = IndexPath(row: row, section: sectionNumber)
                self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
            }
        }
    }
    }

    override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        print("DESELECTED THE CELL AT", hash)

        let section = sections[indexPath.section]
        guard let names = members[section] else { fatalError() }
        let name = names[indexPath.row]
        let hash = sectionNameToHash(section: section, name: name)
        selectedMembers.remove(hash)

        updateCell(atIndexPath: indexPath)
    }

}

【讨论】:

  • 哇,我无话可说,这太棒了,我必须以正确的方式调整它,但它太棒了,非常感谢!
  • 我已经授予你赏金,即使现在它不适用于我的代码,因为如果我希望它甚至可以远程工作,我还有很多重写工作要做,但它肯定会服务路上的人。
  • @Jp4Real 这样做不仅仅是为了积分。通过我的个人资料页面电子邮件向我发送电子邮件寻求帮助。建一个类似的类,我会帮你调试,或者复习。
  • 不是我的意思,我的意思是因为它回答了问题并解决了问题,即使它没有专门直接解决我的问题(由于代码的复杂性)它将使社区受益总的来说,我相信你应该得到荣誉
  • @AlekseyPotapov 现在在 GitHub 上:github.com/dhoerl/TableSectionsAndAnimatedUpdate - 还使用 performBatchUpdates 将 reloadData() 替换为动画删除/插入。
【解决方案2】:

对于 2 号,您可以执行以下操作。取消选择后,您将更改该单元格的accessoryType

func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
  let cell = tableView.cellForRow(at: indexPath)

  for selectedIndexPath in tableview.indexPathForSelectedRow {
    if selectedIndexPath.section == indexPath.section {
      tableview.deselectRow(at: indexPath, animated: true)
      cell?.accessoryType = .none
    }
  }

  return indexPath
}

【讨论】:

  • 不幸的是,这并没有解决我的问题,即使我将它添加到我的代码中,也没有任何反应,代码的行为仍然完全相同,并保持复选标记和值直到我点击它被取消选择
  • 您应该在取消选择后设置accessoryType = .none。查看我的编辑答案
  • 我已经做到了,但仍然没有运气,我明天会继续研究它,谢谢你的帮助,但我现在不能接受它,因为它不能解决我的问题,我现在最大的障碍是在 SelectedIndexPath.section,因为它没有。返回任何东西
  • 没问题。我注意到您在 didSelectRowAtIndexPath 函数中有 accessoryType = .none?应该是 accesoryType = .checkmark 吗?
  • 当您第二次选择它时它必须取消选中,这就是它同时具有 .none 和 .checkmark 的原因
【解决方案3】:

您可以在 selectedIndexPaths 数组中添加新选择行的索引路径之前添加一行

    self.selectedIndexPaths = self.selectedIndexPaths.filter({$0.section != indexPath.section})
    self.selectedIndexPaths.append(indexPath)

当 tableView 重新加载时,由于 cellForRow 中的 if else 块,同一部分中先前选择的单元格的外观将发生变化。

【讨论】:

  • XCode 给了我以下信息:“在调用中缺少参数标签 'using:'”,当我添加它时,它会在 '''{$0.section''' 之前添加它,但后来我得到了一个新的错误:表达式类型“布尔”在没有更多我不完全理解的上下文的情况下模棱两可
  • 看图片,在我这里工作正常imgur.com/a/bALUvPC,你使用的是哪个 swift 版本
  • 我正在使用 Swift 3
  • 检查文档中的语法并使用 append 而不是 add (idk,如果它是你的扩展或什么)
  • 我这样声明 selectedIndexPaths :var selectedIndexPaths: NSMutableArray = [] 你认为这可能与此有关吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-20
  • 1970-01-01
  • 2015-05-31
  • 1970-01-01
  • 1970-01-01
  • 2015-04-10
  • 1970-01-01
相关资源
最近更新 更多