Expand/Collapse 使用 searchController 索引 tableView 单元格

Expand/Collapse indexed tableView cells with searchController

我有一个包含不同子类别(“Algrebra”、“Biology”、“Chemistry”)的 tableView,这些子类别可通过 searchController 进行索引和搜索。我想将这些子类别放入多个类别(“紧急”、“重要”、“不重要”)中,并在点击时 expand/collapse 它们。我还希望将类别编入索引(而不是子类别),但要保持子类别可通过 searchController 进行搜索。

我不知道如何用我的代码正确实现它。

这是我的代码:

类别控制器

class CategoryController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UISearchResultsUpdating {

    private var searchController = UISearchController()

let categories = ["Urgent", "Important", "Not Important"] 

let subcategories = [                                                                  
        Add(category: "Algrebra", categoryImg: #imageLiteral(resourceName: "Algebra.png")),
        Add(category: "Biology", categoryImg: #imageLiteral(resourceName: "Biology.png")),
        Add(category: "Chemistry", categoryImg: #imageLiteral(resourceName: "Chemistry.png")),
    ]
    private var sectionTitles = [String]()
    private var filteredSectionTitles = [String]()
    private var sortedCategory = [(key: String, value: [Add])]()
    private var filteredCategory = [(key: String, value: [Add])]()

  private let tableView: UITableView = {
    let table = UITableView()
    table.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        return table }()

  override func viewDidLoad() {
        super.viewDidLoad()
//TABLEVIEW
        tableView.rowHeight = 50
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        tableView.register(CategoryCell.self, forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        tableView.sectionIndexColor = .black
        tableView.sectionIndexBackgroundColor = .lightGray
        tableView.sectionIndexTrackingBackgroundColor = .gray
        tableView.allowsMultipleSelection = false
//SEARCHCONTROLLER
        self.searchController = UISearchController(searchResultsController: nil)
        self.searchController.searchResultsUpdater = self
        self.searchController.obscuresBackgroundDuringPresentation = false
        self.searchController.searchBar.placeholder = "Search for your category"
        self.searchController.hidesNavigationBarDuringPresentation = false
        self.navigationItem.searchController = self.searchController
        self.navigationItem.hidesSearchBarWhenScrolling = false
        self.navigationItem.title = "Tasks"
        navigationController?.navigationBar.prefersLargeTitles = true
        self.searchController.searchBar.searchTextField.textColor = .label
        
        
        let groupedList = Dictionary(grouping: self.subcategories, by: { String([=11=].category.prefix(1)) })
        self.sortedCategory = groupedList.sorted{[=11=].key < .key}
        
        for tuple in self.sortedCategory {
            self.sectionTitles.append(tuple.key)
        }
    }
//VIEWDIDLAYOUT
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.frame = view.bounds
    }
/// TABLEVIEW
     func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
            return self.filteredSectionTitles[section]
        } else {
            return self.sectionTitles[section]
        }
    }
    func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
            return self.filteredSectionTitles
        } else {
            return self.sectionTitles
        }
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {
            return self.filteredSectionTitles.count
        } else {
            return self.sectionTitles.count
        }
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if self.searchController.isActive && !self.filteredCategory.isEmpty {
            return self.filteredCategory[section].value.count
        } else {
            return self.sortedCategory[section].value.count
        }
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for:indexPath) as UITableViewCell

        cell.imageView?.contentMode = .scaleAspectFit
        if self.searchController.isActive && !self.filteredCategory.isEmpty {
            cell.textLabel?.text = self.filteredCategory[indexPath.section].value[indexPath.row].category
            cell.imageView?.image = self.filteredCategory[indexPath.section].value[indexPath.row].categoryImg
        } else {
            cell.textLabel?.text = self.sortedCategory[indexPath.section].value[indexPath.row].category
            cell.imageView?.image = self.sortedCategory[indexPath.section].value[indexPath.row].categoryImg
            
        }
        return cell
        
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let currentCell = tableView.cellForRow(at: indexPath)! as UITableViewCell
        Add.details.category = (currentCell.textLabel?.text)!
        let secondVC = DateController()
        navigationController?.pushViewController(secondVC, animated: true)
        print(Add.details.category)

    }
func updateSearchResults(for searchController: UISearchController) {
    
    guard let text = self.searchController.searchBar.text else {
        return
    }
    let filteredCategory = self.sortedCategory.flatMap { [=11=].value.filter { [=11=].category.contains(text) } }
    let groupedCategory = Dictionary(grouping: filteredCategory, by: { String([=11=].category.prefix(1)) } )
    self.filteredCategory = []
    self.filteredCategory = groupedCategory.sorted{ [=11=].key < .key }
    
    self.filteredSectionTitles = []
    for tuple in self.filteredCategory {
        self.filteredSectionTitles.append(tuple.key)
    }
    
    self.tableView.reloadData()
}
}

类别单元格

class CategoryCell: UITableViewCell {
    var cellImageView = UIImageView()
    var cellLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: "cell")
            
            cellImageView.translatesAutoresizingMaskIntoConstraints = false
            cellImageView.contentMode = .scaleAspectFit
            cellImageView.tintColor = .systemPink
            contentView.addSubview(cellImageView)
            
            cellLabel.translatesAutoresizingMaskIntoConstraints = false
            cellLabel.font = UIFont.systemFont(ofSize: 20)
            contentView.addSubview(cellLabel)
            
            NSLayoutConstraint.activate([
                cellImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
                cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
                cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
                cellImageView.widthAnchor.constraint(equalToConstant: 44),
                
                cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
                cellLabel.leadingAnchor.constraint(equalTo: cellImageView.trailingAnchor, constant: 10),
                
            ])
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

     }
    
        
}

添加(数据结构)

struct Add {
    static var details: Add = Add()
    var category: String = ""
    
    func getDict() -> [String: Any] {
              let dict = ["category": self.category,
                         
                ] as [String : Any]
               return dict
         }

}

应该有帮助的一些提示...

首先,让我们更改一些命名。

您正在使用“紧急”、“重要”、“不重要”的“类别”作为部分 ...您的“子类别”会更准确描述为“类别”。

我们也可以将 Sections 视为 Category Status

所以,我们将像这样创建一个 enum

enum CategoryStatus: Int, CustomStringConvertible {
    case urgent
    case important
    case notimportant
    
    var description : String {
        switch self {
        case .urgent: return "Urgent"
        case .important: return "Important"
        case .notimportant: return "Not Important"
        }
    }
    
    var star : UIImage {
        switch self {
        case .urgent: return UIImage(named: "star") ?? UIImage()
        case .important: return UIImage(named: "halfstar") ?? UIImage()
        case .notimportant: return UIImage(named: "emptystar") ?? UIImage()
        }
    }
}

然后我们将向类别结构添加一个“状态”属性:

struct MyCategory {
    var name: String = ""
    var categoryImg: UIImage = UIImage()
    var status: CategoryStatus = .important
}

现在,我们可以使用“通俗易懂的语言”完成整个过程:

  • 首先按名称对整个类别列表进行排序
  • 当我们键入搜索字符串时,我们可以通过“名称包含搜索”过滤该列表
  • 什么时候可以按状态对该列表进行分组

所以如果我们开始:

Biology : .important
Chemistry : .urgent
Algebra : .urgent

我们可以对名称进行排序并得到

Algebra : .urgent
Biology : .important
Chemistry : .urgent

然后按状态分组

.urgent
    Algebra
    Chemistry
.important
    Biology

如果我们在搜索字段中输入了“b”,我们会从排序的 ALL 列表开始,然后过滤它:

Algebra : .urgent
Biology : .important

然后按状态分组

.urgent
    Algebra
.important
    Biology

另一个提示:不要使用“完整列表”和“筛选列表”,以及一堆

if self.searchController.isActive && !self.filteredSectionTitles.isEmpty {

块,使用单个排序、过滤和分组列表。

然后该列表将设置为 A) 完整列表(如果没有输入搜索文本)或 B) 过滤后的列表

这是您可以试用的完整示例。我使用了一堆随机主题作为类别,并为每个类别图像使用了圆圈中的数字,并且我使用了 star、halfstar 和 emptystar 的 png。

请注意,这只是 示例代码!。它不是,也不应该被认为是“生产就绪”:

enum CategoryStatus: Int, CustomStringConvertible {
    case urgent
    case important
    case notimportant
    
    var description : String {
        switch self {
        case .urgent: return "Urgent"
        case .important: return "Important"
        case .notimportant: return "Not Important"
        }
    }
    
    var star : UIImage {
        switch self {
        case .urgent: return UIImage(named: "star") ?? UIImage()
        case .important: return UIImage(named: "halfstar") ?? UIImage()
        case .notimportant: return UIImage(named: "emptystar") ?? UIImage()
        }
    }
}

struct MyCategory {
    var name: String = ""
    var categoryImg: UIImage = UIImage()
    var status: CategoryStatus = .important
}

class CategoryController: UIViewController, UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate, UISearchResultsUpdating {
    
    private var searchController = UISearchController()
    
    // array of ALL Categories, sorted by name
    private var sortedCategories: [MyCategory] = []
    
    // this will be either ALL items, or the filtered items
    //  grouped by Status
    private var sortedByStatus = [(key: CategoryStatus, value: [MyCategory])]()
    
    private let tableView = UITableView()
    
    private let noMatchesLabel: UILabel = {
        let v = UILabel()
        v.backgroundColor = .yellow
        v.text = "NO Matches"
        v.textAlignment = .center
        return v
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var items: [MyCategory] = []
        
        // this will be our list of MyCategory objects (they'll be sorted later)
        let itemNames: [String] = [
            "Algebra",
            "Chemistry",
            "Biology",
            "Computer Sciences",
            "Physics",
            "Earth Sciences",
            "Geology",
            "Political Science",
            "Psychology",
            "Nursing",
            "Economics",
            "Agriculture",
            "Communications",
            "Engineering",
            "Foreign Lanuages",
            "English Language",
            "Literature",
            "Libary Sciences",
            "Social Sciences",
            "Visual Arts",
        ]
        
        // create our array of MyCategory
        //  setting every 3rd one to .urgent, .important or .notimportant
        for (str, i) in zip(itemNames, 0...30) {
            let status: CategoryStatus = CategoryStatus.init(rawValue: i % 3) ?? .important
            var img: UIImage = UIImage()
            if let thisImg = UIImage(named: str) {
                img = thisImg
            } else {
                if let thisImg = UIImage(systemName: "\(i).circle") {
                    img = thisImg
                }
            }
            items.append(MyCategory(name: str, categoryImg: img, status: status))
        }
        
        // sort the full list of categories by name
        self.sortedCategories = items.sorted{[=18=].name < .name}
        
        //TABLEVIEW
        tableView.rowHeight = 50
        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        tableView.sectionIndexColor = .black
        tableView.sectionIndexBackgroundColor = .lightGray
        tableView.sectionIndexTrackingBackgroundColor = .gray
        tableView.allowsMultipleSelection = false
        
        tableView.dataSource = self
        tableView.delegate = self
        
        tableView.register(CategoryCell.self, forCellReuseIdentifier: CategoryCell.reuseIdentifier)
        tableView.register(MySectionHeaderView.self, forHeaderFooterViewReuseIdentifier: MySectionHeaderView.reuseIdentifier)
        
        //SEARCHCONTROLLER
        self.searchController = UISearchController(searchResultsController: nil)
        self.searchController.searchResultsUpdater = self
        self.searchController.obscuresBackgroundDuringPresentation = false
        self.searchController.searchBar.placeholder = "Search for your category"
        self.searchController.hidesNavigationBarDuringPresentation = false
        self.navigationItem.searchController = self.searchController
        self.navigationItem.hidesSearchBarWhenScrolling = false
        self.navigationItem.title = "Tasks"
        navigationController?.navigationBar.prefersLargeTitles = true
        self.searchController.searchBar.searchTextField.textColor = .label
        
        // add the no-matches view
        noMatchesLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(noMatchesLabel)
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            tableView.topAnchor.constraint(equalTo: g.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            noMatchesLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
            noMatchesLabel.heightAnchor.constraint(equalToConstant: 120.0),
            noMatchesLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            noMatchesLabel.topAnchor.constraint(equalTo: tableView.frameLayoutGuide.topAnchor, constant: 40.0),
            
        ])
        
        noMatchesLabel.isHidden = true
        
        // call updateSearchResults to build the initial non-filtered data
        updateSearchResults(for: searchController)
        
    }
    
    /// TABLEVIEW
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let v = tableView.dequeueReusableHeaderFooterView(withIdentifier: MySectionHeaderView.reuseIdentifier) as! MySectionHeaderView
        v.imageView.image = self.sortedByStatus[section].key.star
        v.label.text = self.sortedByStatus[section].key.description
        return v
    }
    func sectionIndexTitles(for tableView: UITableView) -> [String]? {
        // first char of each section title
        return (sortedByStatus.map { [=18=].key.description }).compactMap { String([=18=].prefix(1)) }
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        return self.sortedByStatus.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.sortedByStatus[section].value.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CategoryCell.reuseIdentifier, for:indexPath) as! CategoryCell
        
        cell.cellLabel.text = self.sortedByStatus[indexPath.section].value[indexPath.row].name
        cell.cellImageView.image = self.sortedByStatus[indexPath.section].value[indexPath.row].categoryImg
        
        return cell
        
    }
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        // get Category object from data
        let thisCategory: MyCategory = self.sortedByStatus[indexPath.section].value[indexPath.row]
        print("selected:", thisCategory.name, "status:", thisCategory.status)
        
    }
    func updateSearchResults(for searchController: UISearchController) {
        
        var filteredList: [MyCategory] = []
        
        if let text = self.searchController.searchBar.text, !text.isEmpty {
            
            // we have text to search for, so filter the list
            filteredList = self.sortedCategories.filter { [=18=].name.localizedCaseInsensitiveContains(text) }
            
        } else {
            
            // no text to search for, so use the full list
            filteredList = self.sortedCategories
            
        }
        
        // filteredList is now either ALL Categories (no search text entered), or
        //  ALL Categories filtered by search text
        
        // create a dictionary of items grouped by status
        let groupedList = Dictionary(grouping: filteredList, by: { [=18=].status })
        
        // order the grouped list by status
        self.sortedByStatus = groupedList.sorted{[=18=].key.rawValue < .key.rawValue}
        
        // show noMatchesLabel if we have NO matching Categories
        noMatchesLabel.isHidden = self.sortedByStatus.count != 0
        
        // reload the table
        self.tableView.reloadData()
        
    }
}

// simple cell with image view and label
class CategoryCell: UITableViewCell {
    
    static let reuseIdentifier: String = String(describing: self)
    
    var cellImageView = UIImageView()
    var cellLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        cellImageView.translatesAutoresizingMaskIntoConstraints = false
        cellImageView.contentMode = .scaleAspectFit
        cellImageView.tintColor = .systemPink
        contentView.addSubview(cellImageView)
        
        cellLabel.translatesAutoresizingMaskIntoConstraints = false
        cellLabel.font = UIFont.systemFont(ofSize: 20)
        contentView.addSubview(cellLabel)
        
        NSLayoutConstraint.activate([
            
            cellImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            cellImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
            cellImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
            cellImageView.widthAnchor.constraint(equalToConstant: 44),
            
            cellLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            cellLabel.leadingAnchor.constraint(equalTo: cellImageView.trailingAnchor, constant: 10),
            
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        
    }
    
}

// simple reusable section header with image view and label
class MySectionHeaderView: UITableViewHeaderFooterView {
    
    static let reuseIdentifier: String = String(describing: self)
    
    let imageView = UIImageView()
    let label = UILabel()
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        
        imageView.contentMode = .scaleAspectFit
        label.font = .systemFont(ofSize: 20.0, weight: .bold)
        
        [imageView, label].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(v)
        }
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            
            imageView.widthAnchor.constraint(equalToConstant: 24.0),
            imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
            imageView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            imageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12.0),
            label.topAnchor.constraint(equalTo: g.topAnchor),
            label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
        ])
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
}

启动时的外观如下:

然后,当我们输入“t”“e”“ra”时:


编辑

示例代码中的 for (str, i) in zip(itemNames, 0...30) { 块只是生成一些示例项目的简单方法。

要在您的代码中使用它,您可能会这样做:

let items = [
    MyCategory(name: "Algebra", categoryImg: #imageLiteral(resourceName: "Algebra.png"), status: .urgent),                                                                 
    MyCategory(name: "Biology", categoryImg: #imageLiteral(resourceName: "Biology.png"), status: .important),                                                                 
    MyCategory(name: "Chemistry", categoryImg: #imageLiteral(resourceName: "Chemistry.png"), status: .notimportant),
    // and so on                                                                 
]