问题描述
大家好,首先我很抱歉,如果我的问题可能不是很清楚,我将提供任何澄清。
我有一个 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:)
您可以动态地允许/禁止点击单元格。