Search code examples
iosmodel-view-controlleruiviewuicollectionviewuicollectionviewcell

Select FirstIndex in UICollectionView


Hi all I'm working with a UICollectionView which shows 19 cells. The cells show a list of times

enter image description here


I manage my data through a model (TimeSelModel)

TimeSelModel.swift

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

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

let dataSec0 = [ // Dati della sezione 0
    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 = [ // Dati della sezione 1
    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)
]

var cellInteraction assigns the isUserInteractionEnable status of the cells of the collectionView

In the UIView class that contains the collectionView, I created a function for managing the interaction of the cells, that is, all the cells that contain a time lower than the current one must have cellInteraction == false

TimeSelView.swift

private var data: [Section<TimeSelModel>] = []


private func isPreviousTime(_ values: (Int, Int)) -> Bool {
    guard let time = Calendar.current.date(bySettingHour: values.0, minute: values.1, second: 0, of: Date()) else {
        // Non è stato possibile determinare l'orario prescelto
        return false
    }
    
    return time < Date()
}


// MARK: Update Cell Status
    private func updateCellStatus() {
        
        var sec: Int = 0
        var hour: Int = 0
        var minute: Int = 0
                        
        (0..<data.count).forEach { (section) in
            
            sec = section

            (0..<data[section].dataModel.count).forEach { (values) in
                
                hour = data[section].dataModel[values].hour
                minute = data[section].dataModel[values].minute
                
                data[section].dataModel[values].cellInteraction = isPreviousTime((hour, minute)) ? false : true
            }
        }
                
        cv.reloadData()
       
        // GET FIRST INDEX WHERE cellInteraction == TRUE

        if let index = data[sec].dataModel.firstIndex(where: { $0.cellInteraction }) {
            cv.selectItem(at: IndexPath(item: index, section: sec), animated: false, scrollPosition: [])

            print(sec)
            // SEC is always 1 .. this is wrong
        }
    }

As you can see from the image everything works fine but I have problems with the selection.

I need to get the firstIndex of the collectionView where cellInteraction == TRUE.

From the image I showed you should select the cell containing the time 11:30 instead continue to select the cell that contains 12:00, this is because it always keeps returning section 1 inside the loop and I don't understand why

Where am I wrong at this point? why does it always return section 1 when it should (in this case) return section 0 and select the cell containing 11:30?


In the custom cell class I assign the value of cellInteraction to the isUserInteractionEnable of the cell

TimeSelCell.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)
    
    var dataModel: TimeSelModel! {
        didSet {
            
            hourLbl.text = String(format: "%02d", dataModel.hour)
            minuteLbl.text = ":" + String(format: "%02d", dataModel.minute)
            
            isUserInteractionEnabled = dataModel.cellInteraction
            contentView.alpha = dataModel.cellInteraction ? 1 : 0.5
        }
    }

Solution

  • You are only evaluating the last array in your data:

    // MARK: Update Cell Status
    private func updateCellStatus() {
        
        var sec: Int = 0
        var hour: Int = 0
        var minute: Int = 0
        
        (0..<data.count).forEach { (section) in
            
            // set sec equal to section, which will increment each
            //  time through this loop
            sec = section
            
            (0..<data[section].dataModel.count).forEach { (values) in
                
                hour = data[section].dataModel[values].hour
                minute = data[section].dataModel[values].minute
                
                data[section].dataModel[values].cellInteraction = isPreviousTime((hour, minute)) ? false : true
            }
        }
        
        cv.reloadData()
    
        // NOTE:
        //      
        // the var "sec" now equals data.count
        // 
        // 
        
        // GET FIRST INDEX WHERE cellInteraction == TRUE
        
        if let index = data[sec].dataModel.firstIndex(where: { $0.cellInteraction }) {
            cv.selectItem(at: IndexPath(item: index, section: sec), animated: false, scrollPosition: [])
            
            print(sec)
            // SEC is always 1 .. this is wrong
        }
    }
    

    So, when you exit that loop, you want to loop through the sections again, checking for the first match:

        cv.reloadData()
    
        // GET FIRST INDEX WHERE cellInteraction == TRUE
    
        var foundSection: Int = -1
        var foundIndex: Int = -1
        
        for section in 0..<data.count {
            if let index = data[section].dataModel.firstIndex(where: { $0.cellInteraction }) {
                foundSection = section
                foundIndex = index
                break
            }
        }
    
        // if section and index are "-1" (so you'll only need to check one of them)
        //  no object found with cellInteraction == true
        if foundSection == -1 {
            print("No objects found with cellInteraction == true!!!")
        } else {
            print("Found First -- Section:", foundSection, "Index:", foundIndex)
        }
    

    Edit - after comments

    To get the object with its hour/min closest to the current time (hour/min), create a new struct with the section, index, and time difference, then find the object with the smallest difference.

    // MARK: Update Cell Status
    private func updateCellStatus() {
        
        struct DiffStruct {
            var section: Int = 0
            var index: Int = 0
            var timeDiff: TimeInterval = 0
        }
        
        var tempArray: [DiffStruct] = []
        
        var hour: Int = 0
        var minute: Int = 0
        
        (0..<data.count).forEach { (section) in
            
            (0..<data[section].dataModel.count).forEach { (values) in
                
                hour = data[section].dataModel[values].hour
                minute = data[section].dataModel[values].minute
                
                // get a valid Date object
                if let time = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) {
                    // create a DiffStruct object, setting .timeDiff to
                    //  this TimeSelModel's time minus the current time
                    let ds = DiffStruct(section: section, index: values, timeDiff: time.timeIntervalSinceReferenceDate - curTime.timeIntervalSinceReferenceDate)
                    // append it to the tempArray
                    tempArray.append(ds)
                    // set .cellInteraction to TRUE if .timeDiff is greater-than-or-equal to Zero
                    data[section].dataModel[values].cellInteraction = ds.timeDiff >= 0
                } else {
                    // could not create a Date from hour/minute
                    //  so set .cellInteraction to false
                    data[section].dataModel[values].cellInteraction = false
                }
            }
        }
        
        //cv.reloadData()
    
        // instead of:      
        //     GET FIRST INDEX WHERE cellInteraction == TRUE
    
        // we want to:
        //     Get the object where its time is CLOSEST to the current time
        //     without being LATER
    
        // use only Positive timeDiffs (so we're only looking at LATER times)
        let tmp: [DiffStruct] = tempArray.filter({ $0.timeDiff >= 0})
        // get the object with the smallest timeDiff
        if let first = tmp.min(by: { $0.timeDiff < $1.timeDiff } ) {
            print("Closest Match is Sec:", first.section, "Idx:", first.index)
        } else {
            print("No objects found with hour/min greater than current hour/min !!!")
        }
    
    }
    

    Edit 2 -- after more comments

    I re-did the code to make it a little more clear what's going on (also avoided the extra .filter step):

    // MARK: Update Cell Status
    private func updateCellStatus(with curTime: Date) -> (section: Int, index: Int)? {
        
        struct DiffStruct {
            var section: Int = 0
            var index: Int = 0
            var timeDiff: TimeInterval = 0
        }
        
        var tempArray: [DiffStruct] = []
        
        (0..<data.count).forEach { (section) in
            
            (0..<data[section].dataModel.count).forEach { (index) in
                
                let thisModel = data[section].dataModel[index]
                
                let hour = thisModel.hour
                let minute = thisModel.minute
                
                // get a valid Date object
                if let time = Calendar.current.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) {
                    
                    // get difference between thisModel's time and curTime
                    let diff = time.timeIntervalSinceReferenceDate - curTime.timeIntervalSinceReferenceDate
                    
                    if diff >= 0 {
    
                        // if diff is >= 0,
                        //  thisModel's time is LATER than curTime (or equal to)
    
                        // set .cellInteraction to TRUE if .timeDiff is greater-than-or-equal to Zero
                        data[section].dataModel[index].cellInteraction = true
    
                        // create a DiffStruct object
                        let ds = DiffStruct(section: section, index: index, timeDiff: diff)
                        
                        // append it to the tempArray
                        tempArray.append(ds)
                        
                    } else {
    
                        // set .cellInteraction to FALSE
                        data[section].dataModel[index].cellInteraction = false
                        
                    }
    
                } else {
                    // could not create a Date from hour/minute
                    //  so set .cellInteraction to false
                    data[section].dataModel[index].cellInteraction = false
                }
                
            }
            
        }
        
        // we've udpated .cellInteraction for all models, so
        //cv.reloadData()
        
        // get the object with the smallest timeDiff
        if let first = tempArray.min(by: { $0.timeDiff < $1.timeDiff } ) {
            return (first.section, first.index)
        }
        
        // No objects found with hour/min greater than current hour/min !!!
        return nil
    
    }
    

    You can test that out like this:

    class TestViewController: UIViewController {
    
        private var data: [Section<TimeSelModel>] = []
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            data.append(Section(dataModel: dataSec0))
            data.append(Section(dataModel: dataSec1))
            
            // start at 09:23
            var currentTime: Date = Calendar.current.date(bySettingHour: 9, minute: 23, second: 0, of: Date())!
            
            for _ in 1...24 {
                
                let h = Calendar.current.component(.hour, from: currentTime)
                let m = Calendar.current.component(.minute, from: currentTime)
                
                if let p = updateCellStatus(with: currentTime) {
                    let theModel = data[p.section].dataModel[p.index]
                    print("Closest to \(h):\(m) is Section:", p.section, "Index:", p.index, "--", theModel.time)
                } else {
                    print("\(h):\(m) is Later than all times!!!")
                }
                
                // add a half-hour
                currentTime = currentTime.addingTimeInterval(30 * 60)
                
            }
    
        }
    
        struct Section<T> { var dataModel: [T] }
        
        struct TimeSelModel {
            let hour: Int
            let minute: Int
            var time: String { "\(hour):\(minute)" }
            var cellInteraction: Bool = true
        }
        
        var dataSec0 = [ // Dati della sezione 0
            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)
        ]
        
        var dataSec1 = [ // Dati della sezione 1
            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)
        ]
    
    }