我有收集视图。当我触摸单元格时,收集单元类的动画
CarouselCell
播放。当我按单元格时,下载文件的过程以方法didSelectItemAt
开始。为了更新过程,我从这里使用代码reloadItem(indexPath: IndexPath)
问题是,如果下载文件时,我尝试触摸单元格,动画将播放一次,例如跳跃或挂钩,然后它根本无法使用触摸直到下载文件为止。另外,在下载过程中,再次单击单元格后,该方法didSelectItemAt
在下载文件之前不会调用。在文件下载过程中未调用didSelectItemAt
方法的事实很好。这样可以防止同一文件多次下载。但是问题在于,我不明白为什么会发生这种情况,因为我没有在任何地方写这样的代码。如何修复动画,为什么在文件下载过程中未调用didSelectItemAt
?
代码:
i删除了所有相关的代码以节省相关的空间并用计时器替换为模拟文件下载进度
收集:
struct DownloadItem {
var item: Int
var state: String = ".none"
var progress: Float = 0.0
init(item: Int) { self.item = item }
}
class CarouselController: UIViewController, UICollectionViewDelegate {
var progress = 0.0
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
let sections = Bundle.main.decode([Section].self, from: "carouselData.json")
var items = [DownloadItem]()
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
setupScrollView()
setupDownloadItems()
}
func setupScrollView() {
collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .centeredHorizontally, animated: false)
}
func setupDownloadItems() {
let count = dataSource!.snapshot().numberOfItems
for index in 0...count { items.append(DownloadItem(item: index)) }
}
func createDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch self.sections[indexPath.section].identifier {
case "carouselCell":
let cell = self.configure(CarouselCell.self, with: item, for: indexPath)
if indexPath.row < self.items.count {
let item = self.items[indexPath.row]
cell.title.text = "\(String(format: "%.f%%", item.progress * 100))"
}
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with item: Item, for indexPath: IndexPath) -> T {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError("\(cellType)") }
cell.configure(with: item)
return cell
}
func reloadData() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(sections)
for section in sections { snapshot.appendItems(section.item, toSection: section) }
dataSource?.apply(snapshot)
}
func setupCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.isScrollEnabled = false
collectionView.delegate = self
collectionView.contentInsetAdjustmentBehavior = .never
view.addSubview(collectionView)
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
createDataSource()
reloadData()
}
func createCompositionalLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupWidth = (layoutEnvironment.container.contentSize.width * 1.05)/3
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth),
heightDimension: .absolute(groupWidth))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: (layoutEnvironment.container.contentSize.height/2) - (groupWidth/2),
leading: 0,
bottom: 0,
trailing: 0)
section.interGroupSpacing = 64
section.orthogonalScrollingBehavior = .groupPagingCentered
section.contentInsetsReference = .none
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0)
let minScale: CGFloat = 0.7
let maxScale: CGFloat = 1.1
let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width), minScale)
item.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return section
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("-")
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progress <= 1.0 else {
timer.invalidate()
self.progress = 0.0
return
}
self.progress += 0.01
self.items[indexPath.row].progress = Float(self.progress)
self.reloadItem(indexPath: .init(row: indexPath.row, section: 0))
}
}
func reloadItem(indexPath: IndexPath) {
guard let needReloadItem = dataSource!.itemIdentifier(for: indexPath) else { return }
guard var snapshot = dataSource?.snapshot() else { return }
snapshot.reloadItems([needReloadItem])
dataSource?.apply(snapshot, animatingDifferences: false)
}
}
CELL
class CarouselCell: UICollectionViewCell, SelfConfiguringCell {
static let reuseIdentifier: String = "carouselCell"
var topSpaceView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var imageView: UIImageView = {
let image = UIImageView()
image.contentMode = .scaleAspectFill
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var titleImageView: UIImageView = {
let image = UIImageView()
image.contentMode = .scaleAspectFill
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var textView: UIView = {
let view = UIView()
view.backgroundColor = .blue
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 3
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var title: UILabel = {
let label = UILabel()
label.textColor = .white
label.textAlignment = .center
label.sizeToFit()
label.font = UIFont(name: "Marker Felt", size: 24)
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
var progressView: UIProgressView = {
let view = UIProgressView(progressViewStyle: .bar)
view.center = view.center
view.setProgress(0.0, animated: true)
view.trackTintColor = .clear
view.tintColor = .white.withAlphaComponent(0.15)
view.clipsToBounds = true
view.layer.borderColor = UIColor.red.cgColor
view.layer.borderWidth = 3
view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var bottomSpaceView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var stackView: UIView = {
let stack = UIView()
stack.layer.shadowColor = UIColor.black.cgColor
stack.layer.shadowOffset = CGSize.zero
stack.layer.shadowOpacity = 0.6
stack.layer.shadowRadius = 3
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
override var isHighlighted: Bool {
didSet { if oldValue == false && isHighlighted { highlight() }
else if oldValue == true && !isHighlighted { unHighlight() }
}
}
var imageTask: AnyCancellable?
var titleImageTask: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
titleImageView.image = nil
imageTask?.cancel()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
}
required init?(coder: NSCoder) {
fatalError("error")
}
func setupViews() {
stackView.addSubview(topSpaceView)
stackView.addSubview(imageView)
stackView.addSubview(titleImageView)
stackView.addSubview(textView)
stackView.addSubview(bottomSpaceView)
contentView.addSubview(stackView)
textView.addSubview(progressView)
textView.addSubview(title)
stackView.sendSubview(toBack: textView)
textView.bringSubview(toFront: title)
}
func setupConstraints() {
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 0).isActive = true
topSpaceView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: 0).isActive = true
topSpaceView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.07).isActive = true
topSpaceView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
topSpaceView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
imageView.topAnchor.constraint(equalTo: topSpaceView.bottomAnchor, constant: 0).isActive = true
imageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.7).isActive = true
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true
titleImageView.topAnchor.constraint(equalTo: topSpaceView.bottomAnchor, constant: 0).isActive = true
titleImageView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.7).isActive = true
titleImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 0).isActive = true
titleImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: 0).isActive = true
textView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.16).isActive = true
textView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 24).isActive = true
textView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -24).isActive = true
textView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -6).isActive = true
textView.layer.cornerRadius = contentView.frame.size.height*0.16/2
title.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 0).isActive = true
title.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: 0).isActive = true
title.topAnchor.constraint(equalTo: textView.topAnchor, constant: 0).isActive = true
title.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0).isActive = true
progressView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.16).isActive = true
progressView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 24).isActive = true
progressView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -24).isActive = true
progressView.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -6).isActive = true
progressView.layer.cornerRadius = contentView.frame.size.height*0.16/2
bottomSpaceView.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0).isActive = true
bottomSpaceView.heightAnchor.constraint(equalTo: contentView.heightAnchor, multiplier: 0.07).isActive = true
bottomSpaceView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
bottomSpaceView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
}
private func animateScale(to scale: CGFloat, duration: TimeInterval) {
UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [.beginFromCurrentState], animations: {
self.stackView.transform = .init(scaleX: scale, y: scale)
self.textView.transform = .init(scaleX: scale, y: scale)
}, completion: nil)
}
func highlight() {
animateScale(to: 0.9, duration: 0.4)
}
func unHighlight() {
animateScale(to: 1, duration: 0.38)
}
func configure(with item: Item) {
title.text = item.title
textView.backgroundColor = .green
textView.layer.borderColor = UIColor.green.cgColor
progressView.layer.borderColor = UIColor.green.cgColor
titleImageTask = Future<UIImage?, Never>() { promise in
UIImage(named: item.titleImage)?.prepareForDisplay(completionHandler: { loadedImage in
promise(Result.success(loadedImage))
})
}
.receive(on: DispatchQueue.main)
.sink { image in
self.titleImageView.image = image
}
imageTask = Future<UIImage?, Never>() { promise in
UIImage(named: item.image)?.prepareForDisplay(completionHandler: { loadedImage in
promise(Result.success(loadedImage))
})
}
.receive(on: DispatchQueue.main)
.sink { image in
self.imageView.image = image
}
}
}
也尝试使用
touchesBegan
/touchesEnded
而不是iShighlighthighted,但结果是相同的:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
highlight()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
unHighlight()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
unHighlight()
}
Additional:
extension Bundle {
func decode<T: Decodable>(_ type: T.Type, from file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
let decoder = JSONDecoder()
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
return loaded
}
}
protocol SelfConfiguringCell {
static var reuseIdentifier: String { get }
func configure(with item: Item)
}
public struct Section: Decodable, Hashable {
let index: Int
let identifier: String
let title: String
let subtitle: String
let item: [Item]
}
public struct Item: Decodable, Hashable {
let index: Int
let title: String
let image: String
let titleImage: String
let backgroundColor: String
let borderColor: String
}
json
[
{
"index": 1,
"identifier": "carouselCell",
"title": "",
"subtitle": "",
"item": [
{
"index": 1,
"title": "0",
"image": "cover1",
"titleImage": "title1",
"backgroundColor": "5896CC",
"borderColor": "3274AF",
},
{
"index": 2,
"title": "0",
"image": "cover2",
"titleImage": "title2",
"backgroundColor": "895138",
"borderColor": "703A22",
},
{
"index": 3,
"title": "0",
"image": "cover3",
"titleImage": "title3",
"backgroundColor": "804136",
"borderColor": "62322A",
},
{
"index": 4,
"title": "0",
"image": "cover4",
"titleImage": "title4",
"backgroundColor": "3E78A3",
"borderColor": "2A597D",
},
]
},
]
isHighlighted
是
true
isHighlighted
可能会使单元的状态管理混淆。我可能建议您不要重新加载整个单元格,而是要使用单元格使用不同的机制来根据下载的状态进行自我更新。例如,也许添加一个the的属性,使该单元观察
progress
的属性。或使用通知中心随着进度的通知,并让细胞观察这些通知。
这可能是次要的问题,但是我发现当所有单元子视图指定
Item
的所有单元格时,小区的选择更为可靠(当然,除非您需要这些子视图的用户互动,否则至少在这一点上不是这种情况)。解决先前的问题可能会消除这一问题,而只是一个选择。
一些无关的观察:
我建议不要使用
isUserInteractionEnabled
确定正在更新的单元格。您要确保您的异步过程将不受收集视图中单元格的插入或删除的影响。
不必说,一旦将进度更新移至单元本身,您还将确保正确处理要重新使用的单元格以进行不同的下载(因为有问题的单元可能会滚动滚动并重复使用)。
false
方法的事实很好。
我建议您确实希望在选择单元格时被调用。现在,显然,您将需要跟踪下载是否已经开始,以免您启动重复下载。但是,不要依靠没有调用的当前行为,因为这是一个错误。 注意,您的视图控制器具有IndexPath
didSelectItemAt