在 UICollectionView 中找到值为“isUserInteractionEnabled == true”的第一个单元格

问题描述

大家好,首先我很抱歉,如果我的问题可能不是很清楚,我将提供任何澄清。

我有一个 UIView 类 (TimeSelectorView),其中包含一个UICollectionView 填充的 var data: [Section<TimeSelModel>] = []

我插入了另一个名为 var reservationsData: [(Int,Int)] = [] 的数组,其中包含用户选择的单元格。

如果 reservationsData array 包含的值等于 var data array,则相关单元格必须不可点击。

到目前为止一切正常,但我有一个问题..我想在第一个可用的单元格上设置一个初始选择,其值为 isUserInteractionEnabled == true

如何在方法外找到拥有 isUserInteractionEnabled == true 的单元格的第一个可用索引

func collectionView (_ collectionView: UICollectionView,cellForItemAt indexPath: IndexPath) -> UICollectionViewCell?

我知道在方法 init(frame: CGRect) 中,collectionView 尚未创建,因此我无法获取单元格索引,但此时我该如何解决

我需要当用户打开应用时,他们可以看到第一个单元格的可用选项,值为 isUserInteractionEnabled == true

你能帮我理解一下吗?


TimeModel.swift

import Foundation

struct Section<T> { let dataModel: [T] }

struct TimeSelModel {
    let hour: Int
    let minute: Int
    var time: String { "\(hour):\(minute)" }
}

let dataSec0 = [
    TimeSelModel(hour: 09,minute: 30),TimeSelModel(hour: 10,minute: 00),TimeSelModel(hour: 11,TimeSelModel(hour: 15,TimeSelModel(hour: 16,TimeSelModel(hour: 17,minute: 00)
]

let dataSec1 = [
    TimeSelModel(hour: 12,TimeSelModel(hour: 12,TimeSelModel(hour: 13,TimeSelModel(hour: 14,TimeSelModel(hour: 18,TimeSelModel(hour: 19,minute: 00)
]

TimeSelectorView.swift

private var data: [Section<TimeSelModel>] = []
private var reservationsData: [(Int,Int)] = []
private var listern: ListenerRegistration!

override init(frame: CGRect) {
    super.init(frame: frame)
    commonInit()
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
    commonInit()
}

private func commonInit() -> Void {
    fetchData()
    getReservationData(for: .currentDay)
    selectAvaiability()
}

private func fetchData() -> Void {
     data = [Section(dataModel: dataSec0),Section(dataModel: dataSec1)] }

private func getSection() -> Int {
    let interval1 = Date().checkTimeBetweenInterval(from: (9,0),to: (11,30))
    let interval2 = Date().checkTimeBetweenInterval(from: (14,30),to: (17,0))
   
    return interval1 || interval2 ? 0 : 1
}
    

private func selectAvaiability() -> Void {
     let section = getSection()

     // Select the first cell with time = or > to the current time and which does not have a value of isUserInteraceEnabled == false
    //cv.selectItem(at: IndexPath(item: ????,section: section),animated: false,scrollPosition: [])
}

// MARK: - UICollectionView Datasource
// MARK: -

extension TimeSelView: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int { data.count }
    
    func collectionView(_ collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int {
        data[section].dataModel.count }
    
    func collectionView(_ collectionView: UICollectionView,cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TimeSelCell.cellID,for: indexPath) as! TimeSelCell
        
        var cellData = data[indexPath.section].dataModel
        cell.dataModel = cellData[indexPath.item]

        if isCurrentDate {
            
            cell.status =
                Date().checkTime(hour: cellData[indexPath.item].hour,minute: cellData[indexPath.item].minute) || Date().isClosingDay ||
                isAReservations(data: (cellData[indexPath.item].hour,cellData[indexPath.item].minute)) ? .disabled : .enabled
        
         }
            
        return cell

        }

    private func isAReservations(data: (Int,Int)) -> Bool {
        reservationsData.contains(where: { $0.0 == data.0 && $0.1 == data.1}) }

 }

extension TimeSelView {
    
    // MARK: Query Periods
    // Gestione del periodo da considerare per la query
    enum Periods {
        case currentDay,future
        
        var query: Query {
            
            let calendar = Calendar.current
            let components = calendar.dateComponents ([.year,.month,.day],from: Date ())
            let startToday = calendar.date (from: components)!
            let endToday = calendar.date (byAdding: .day,value: 1,to: startToday)!
            
            let rootQuery = Service.Database.reservationRoot
            
            switch self {
            
            case .currentDay: return rootQuery.whereField("date",isGreaterThan: startToday).whereField("date",isLessthan: endToday)
            case .future : return rootQuery.whereField("date",isGreaterThan: endToday)
                
            }
        }
    }
    
    // MARK: - Reservation Query
    // Recuperiamo tutte le prenotazioni effettuate per un periodo specifico
    private func getReservationsDataFromQuery(periods: Periods,completed: @escaping () ->()) -> Void {
        
        // Effetuiamo una query in base al perido in cui ci troviamo ovvero giorno corrente oppure giorni futuri
        // Aggiungiamo un ascoltatore per monitorare tutti i cambiamenti che vengono effettuati al database durante le prenotazioni degli uitenti in modo tale da mantenere aggiornata la collectionView
        listern = periods.query.addSnapshotListener { (querySnapshot,error) in
            
            guard error == nil else  {
                print("Errore durante il recupero delle prenotazioni \(error!.localizedDescription)")
                return }
            
            for document in querySnapshot!.documents {
                                   
                // Una volta ottenuti i dati andiamo a recuperare solo i valori di orario che verranno aggiunti all'array che gestisce lo stato delle celle della collectionView
                if let timestamp = document.data()["date"] as? Timestamp {
                    let hour = timestamp.dateValue().component(.hour)
                    let minute = timestamp.dateValue().component(.minute)
                    // Aggiunta dei dati all'array
                    self.prepareReservationsData((hour,minute))
                    completed()

                }
            }
        }
    }
    
    // MARK: -
    private func getReservationData(for periods: Periods) -> Void {
        // Controlla lo stato dell'utente per rimuovere l'ascoltatore della query quando non abbiamo bisogno di interpellare il database altrimenti effettua la query
        NotificationCenter.default.addobserver(forName:.AuthStateDidChange,object: Auth.auth(),queue: nil) { _ in
            
            /// Service.currentUser == nil -> Rimuove l'ascoltatore
            /// Service.currentUser != nil -> Effettua la query e ricalcola la collectionView con i nuovi dati
            if  Service.currentUser == nil { self.listern.remove() }
            else {
                self.getReservationsDataFromQuery(periods: periods) {
                    self.cv.reloadData()
                }
            }
        }
    }
    
    private func prepareReservationsData( _ value: (Int,Int)) -> Void {
        reservationsData += [(value.0,value.1)]
    }
    
 }

TimeSelectorCell.swift

class TimeSelCell: UICollectionViewCell {
    
    static let cellID = "time.selector.cell.identifier"
    
    private let minuteLbl = UILabel(font: .systemFont(ofSize: 13),textColor: .white,textAlignment: .center)
    
    private let hourLbl = UILabel(font: .boldSystemFont(ofSize: 17),textAlignment: .center)
    
    // Tiene traccia dello stato della cella
    /// "disabled" -> Le celle non sono utilizzabili e il valore all'interno è barrato
    /// "enabled" -> Le celle sono utilizzabili per la prenotazione
    
    enum Status { case disabled,enabled }

    var dataModel: TimeSelModel! {
        
        didSet {
            
            hourLbl.text = String(format: "%02d",dataModel.hour)
            minuteLbl.text = ":" + String(format: "%02d",dataModel.minute)
        }
    }
    
    var status: Status {
       
        didSet {
                        
            switch status {
            
            case .disabled :
                isUserInteractionEnabled = false
            case .enabled :
                isUserInteractionEnabled = true
                
            }
            
            hourLbl.attributedText = crossedOut(text: hourLbl.text!)
            minuteLbl.attributedText = crossedOut(text: minuteLbl.text!)

            contentView.alpha = isUserInteractionEnabled ? 1 : 0.5
        }
    }
    
    override var isSelected: Bool {
        
        didSet {
            
            UIView.animate(withDuration: 0.3) {
                self.backgroundColor = self.isSelected ? K.Colors.greenAlpha : .systemFill }
            self.layer.borderColor = self.isSelected ? K.Colors.green.cgColor : UIColor.clear.cgColor
            self.layer.borderWidth = self.isSelected ? 1 : 0
        }
    }
    
    var isPrevIoUsDate: Bool = false
    var isCurrentDate: Bool = true
    
    override init(frame: CGRect) {
        self.status = .disabled

        super.init(frame: frame)
        
        backgroundColor = .systemFill
        layer.cornerRadius =  3
        
        contentView.addSubview(hourLbl)
        contentView.addSubview(minuteLbl)
        
        hourLbl.anchor(top: contentView.topAnchor,leading: contentView.leadingAnchor,bottom: contentView.centerYAnchor,trailing: contentView.trailingAnchor,padding: .init(top: 9,left: 0,bottom: -8,right: 0))

        minuteLbl.anchor(top: contentView.centerYAnchor,bottom: contentView.bottomAnchor,padding: .init(top: -5,bottom: 7,right: 0))
        
    }
    
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}

private extension TimeSelCell {
    
    func crossedOut(text: String) -> NSMutableAttributedString {
        let attributeString: NSMutableAttributedString =  NSMutableAttributedString(string: "\(text)")
        attributeString.addAttribute(NSAttributedString.Key.strikethroughStyle,value:  isUserInteractionEnabled ? 0 : 1,range: NSMakeRange(0,attributeString.length))
        return attributeString
    }
}

解决方法

数据源的多个数组是非常糟糕的做法。

更好的模型是自定义结构

struct Slot {
    let name : String
    var isSaved = false
}

var slots = [Slot(name: "slot1"),Slot(name: "slot2"),Slot(name: "slot3"),Slot(name: "slot4"),Slot(name: "slot5"),Slot(name: "slot6")]

func collectionView(_ collectionView: UICollectionView,numberOfItemsInSection section: Int) -> Int { slots.count }

func collectionView(_ collectionView: UICollectionView,cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SlotCell.cellID,for: indexPath) as! SlotCell

    let slot = slots[indexPath.item]
    cell.lbl.text = slot.name
    cell.isUserInteractionEnabled = !slot.isSaved
    return cell
}
 

要查找第一个保存的项目,请询问模型,而不是视图

let firstSavedItem = slots.first{ $0.isSaved }
,

简短的回答是您可以遍历集合视图的可见单元格,然后获取您正在查找的单元格的索引路径。像这样的事情可能会奏效:

func getThatSpecialIndexIndex(collectionView: UICollectionView) -> IndexPath? {
    let cellsThatHaveUserInteractionEnabled = collectionView.visibleCells.filter { $0.isUserInteractionEnabled }
    let indexPaths = cellsThatHaveUserInteractionEnabled.compactMap { collectionView.indexPath(for: $0) }
    return indexPaths.min { left,right -> Bool in
        if left.section < right.section { return true }
        else if left.section > right.section { return false }
        else { return left.row < right.row }
    }
}

但问题在于它只会包含“可见”单元格。并且集合视图旨在重用单元格。这意味着作为 UIView 子类的单元格实际上可以在不同时间占用不同的索引,具体取决于用户与集合视图的交互方式。

话虽如此,最好使用您的数据源来查找您的索引路径。从您的代码中可以看出以下内容:

cell.isUserInteractionEnabled == savedSlot.contains(slotArray[indexPath.item])

所以基本上看起来你应该能够通过使用重新创建你的索引路径

let index = slotArray.firstIndex { savedSlot.contains($0) }
let indexPath: IndexPath(section: 0,row: index)
,

您不应该在 UICollectionViewCell 上使用“isUserInteractionEnabled”。

而是尝试查看collectionView(_:shouldSelectItemAt:)

您可以动态地允许/禁止点击单元格。