kAiN
kAiN

Reputation: 2773

Find the first cell with value "isUserInteractionEnabled == true" in UICollectionView

Hello everyone first of all I apologize if my question may not be very clear I will be available for any clarification.

I have a UIView class (TimeSelectorView) which contains a UICollectionView which is populated with var data: [Section<TimeSelModel>] = [] .

I inserted another array called var reservationsData: [(Int, Int)] = [] which contains the cell selection by the user.

If the reservationsData array contains values ​​equal to the var data array, the cells in question must not be clickable.

So far everything works but I have a problem .. I would like to set up an initial selection on the first available cell which has the value of isUserInteractionEnabled == true

How can I find the first available Index of the cell that owns isUserInteractionEnabled == true outside the method

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

I know that in the method init(frame: CGRect), the collectionView has not been created yet so I can't get the cells index but at this point how can I solve?

I need that when the user opens the app they can see the selection available for the first cell with value isUserInteractionEnabled == true

Can you help me understand this?


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: 10, minute: 30),
    TimeSelModel(hour: 11, minute: 00),
    TimeSelModel(hour: 11, minute: 30),
    TimeSelModel(hour: 15, minute: 00),
    TimeSelModel(hour: 15, minute: 30),
    TimeSelModel(hour: 16, minute: 00),
    TimeSelModel(hour: 16, minute: 30),
    TimeSelModel(hour: 17, minute: 00)
]

let dataSec1 = [
    TimeSelModel(hour: 12, minute: 00),
    TimeSelModel(hour: 12, minute: 30),
    TimeSelModel(hour: 13, minute: 00),
    TimeSelModel(hour: 14, minute: 00),
    TimeSelModel(hour: 14, minute: 30),
    TimeSelModel(hour: 17, minute: 30),
    TimeSelModel(hour: 18, minute: 00),
    TimeSelModel(hour: 18, minute: 30),
    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), textColor: .white, 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, leading: contentView.leadingAnchor, bottom: contentView.bottomAnchor, trailing: contentView.trailingAnchor, padding: .init(top: -5, left: 0, 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
    }
}

Upvotes: 0

Views: 273

Answers (3)

CZ54
CZ54

Reputation: 5588

You shouldn't messed up with "isUserInteractionEnabled" on a UICollectionViewCell.

Instead try to have a look on collectionView(_:shouldSelectItemAt:)

It will give you the possibility to dynamically allow/disallow the tap on the cell.

Upvotes: 0

Matic Oblak
Matic Oblak

Reputation: 16774

Short answer would be that you can iterate through visible cells of collection view and then get an index path of a cell that you are looking for. Something like this may do the trick:

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

But the problem with this is that it will only include "visible" cells. And collection view is designed to reuse cells. That means that a cell as subclass of UIView can actually occupy different indices at different times depending on how user interacts with collection view.

That being said it is best to use your data source to find your index path. From your code it can be seen the following:

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

So basically it seem like you should be able to recreate your index path by using

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

Upvotes: 0

vadian
vadian

Reputation: 285082

Multiple arrays for the data source is pretty bad practice.

A better model is a custom struct

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
}
 

To find the first saved item ask the model, not the view

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

Upvotes: 1

Related Questions