I have a collection view. When I touch the cell, the animation from the collection view cell class CarouselCell
playing. When I press on a cell, the process of downloading the file begins in the method didSelectItemAt
. To update the process I use the code from here reloadItem(indexPath: IndexPath)
The problem is that if while downloading a file I try to touch a cell, the animation will play once, like a jump or hitch, and then it won’t work at all when touched until the file is downloaded. Also, during downloading, when the cell is clicked again, the method didSelectItemAt
will not be called until the file is downloaded. The fact that the didSelectItemAt
method is not called during file downloading is good. This prevents the same file from being downloaded multiple times. But the problem is that I don’t understand why this happens because I haven’t written such code anywhere. How to fix the animation and why didSelectItemAt
not called during the file is downloading?
code:
I removed all the download related code to save space in question and replaced it with a timer to simulate the file download progress
collection:
class CarouselController: UIViewController, UICollectionViewDelegate {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
let sections = Bundle.main.decode([Section].self, from: "carouselData.json")
var progressA = 0.0
override func viewDidLoad() {
super.viewDidLoad()
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()
}
@objc 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)
cell.title.text = "\(self.progressA)"
print(self.progressA)
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progressA <= 1.0 else {
timer.invalidate()
self.progressA = 0.0
return
}
self.progressA += 0.01
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)
}
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
return section
}
}
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)
}
}
cell
import Combine
class CarouselCell: UICollectionViewCell, SelfConfiguringCell {
static let reuseIdentifier: String = "carouselCell"
var imageView: UIImageView = {
let image = UIImageView()
image.contentMode = .scaleToFill
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var textView: UIView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var title: UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override var isHighlighted: Bool {
didSet { if oldValue == false && isHighlighted { animateScale(to: 0.9, duration: 0.4) }
else if oldValue == true && !isHighlighted { animateScale(to: 1, duration: 0.38) }
}
}
var imageTask: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
imageTask?.cancel()
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(imageView)
contentView.addSubview(textView)
textView.addSubview(title)
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, 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
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
}
private func animateScale(to scale: CGFloat, duration: TimeInterval) {
UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [.beginFromCurrentState], animations: {
self.imageView.transform = .init(scaleX: scale, y: scale)
self.textView.transform = .init(scaleX: scale, y: scale)
}, completion: nil)
}
func configure(with item: Item) {
title.text = "item.title"
textView.backgroundColor = .green
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 = UIImage(named: "cover2")
}
}
required init?(coder: NSCoder) {
fatalError("error")
}
}
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 identifier: String; let item: [Item] }
public struct Item: Decodable, Hashable { let index: Int; let title: String; let image: String }
[ {
"identifier": "carouselCell",
"item": [
{
"index": 1,
"title": "0",
"image": "cover1",
},
] }, ]
based on @Rob's answer
KVO variant (not working)
I get progress inside the observer, but the cell text is not updated. Why?
var nameObservation: NSKeyValueObservation?
@objc dynamic var progress = 0.0
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)
self.nameObservation = self.observe(\.progress, options: .new) { vc, change in
cell.title.text = "\(self.progress)"
}
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progress <= 1.0 else {
timer.invalidate()
self.progress = 0.0
return
}
}
}
Notification variant (not working).
collection is despairing after press
var nameObservation: NSKeyValueObservation?
@objc 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)
cell.title.text = "\(self.progressA)"
print(self.progressA)
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
NotificationCenter.default.addObserver(
self, selector: #selector(self.createDataSource),
name: Notification.Name(rawValue: "sound-CarouselController"), object: nil)
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progressA <= 1.0 else {
timer.invalidate()
self.progressA = 0.0
return
}
self.progressA += 0.01
NotificationCenter.default.post(name: Notification.Name(rawValue: "sound-CarouselController"), object: nil)
}
}
I have a collection view. When I touch the cell, the animation from the collection view cell class CarouselCell
playing. When I press on a cell, the process of downloading the file begins in the method didSelectItemAt
. To update the process I use the code from here reloadItem(indexPath: IndexPath)
The problem is that if while downloading a file I try to touch a cell, the animation will play once, like a jump or hitch, and then it won’t work at all when touched until the file is downloaded. Also, during downloading, when the cell is clicked again, the method didSelectItemAt
will not be called until the file is downloaded. The fact that the didSelectItemAt
method is not called during file downloading is good. This prevents the same file from being downloaded multiple times. But the problem is that I don’t understand why this happens because I haven’t written such code anywhere. How to fix the animation and why didSelectItemAt
not called during the file is downloading?
code:
I removed all the download related code to save space in question and replaced it with a timer to simulate the file download progress
collection:
class CarouselController: UIViewController, UICollectionViewDelegate {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
let sections = Bundle.main.decode([Section].self, from: "carouselData.json")
var progressA = 0.0
override func viewDidLoad() {
super.viewDidLoad()
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()
}
@objc 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)
cell.title.text = "\(self.progressA)"
print(self.progressA)
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progressA <= 1.0 else {
timer.invalidate()
self.progressA = 0.0
return
}
self.progressA += 0.01
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)
}
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
return section
}
}
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)
}
}
cell
import Combine
class CarouselCell: UICollectionViewCell, SelfConfiguringCell {
static let reuseIdentifier: String = "carouselCell"
var imageView: UIImageView = {
let image = UIImageView()
image.contentMode = .scaleToFill
image.translatesAutoresizingMaskIntoConstraints = false
return image
}()
var textView: UIView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
var title: UILabel = {
let label = UILabel()
label.textColor = .white
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
override var isHighlighted: Bool {
didSet { if oldValue == false && isHighlighted { animateScale(to: 0.9, duration: 0.4) }
else if oldValue == true && !isHighlighted { animateScale(to: 1, duration: 0.38) }
}
}
var imageTask: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
imageTask?.cancel()
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(imageView)
contentView.addSubview(textView)
textView.addSubview(title)
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, 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
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
}
private func animateScale(to scale: CGFloat, duration: TimeInterval) {
UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [.beginFromCurrentState], animations: {
self.imageView.transform = .init(scaleX: scale, y: scale)
self.textView.transform = .init(scaleX: scale, y: scale)
}, completion: nil)
}
func configure(with item: Item) {
title.text = "item.title"
textView.backgroundColor = .green
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 = UIImage(named: "cover2")
}
}
required init?(coder: NSCoder) {
fatalError("error")
}
}
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 identifier: String; let item: [Item] }
public struct Item: Decodable, Hashable { let index: Int; let title: String; let image: String }
[ {
"identifier": "carouselCell",
"item": [
{
"index": 1,
"title": "0",
"image": "cover1",
},
] }, ]
based on @Rob's answer
KVO variant (not working)
I get progress inside the observer, but the cell text is not updated. Why?
var nameObservation: NSKeyValueObservation?
@objc dynamic var progress = 0.0
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)
self.nameObservation = self.observe(\.progress, options: .new) { vc, change in
cell.title.text = "\(self.progress)"
}
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progress <= 1.0 else {
timer.invalidate()
self.progress = 0.0
return
}
}
}
Notification variant (not working).
collection is despairing after press
var nameObservation: NSKeyValueObservation?
@objc 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)
cell.title.text = "\(self.progressA)"
print(self.progressA)
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
NotificationCenter.default.addObserver(
self, selector: #selector(self.createDataSource),
name: Notification.Name(rawValue: "sound-CarouselController"), object: nil)
_ = Timer.scheduledTimer(withTimeInterval: 0.10, repeats: true) { timer in
guard self.progressA <= 1.0 else {
timer.invalidate()
self.progressA = 0.0
return
}
self.progressA += 0.01
NotificationCenter.default.post(name: Notification.Name(rawValue: "sound-CarouselController"), object: nil)
}
}
Share
Improve this question
edited Jan 30 at 15:46
User
asked Jan 29 at 18:48
UserUser
1951 gold badge1 silver badge11 bronze badges
3
|
1 Answer
Reset to default 2There are a few issues:
The reloading of a cell while
isHighlighted
istrue
can confuse the cell’sisHighlighted
state management.I might advise that you refrain from reloading of the whole cell, and instead just have the cell use a different mechanism for updating itself based upon the state of the download. E.g., maybe add a
progress
property for the have the cell observe a published property. Or use the notification center post notifications with the progress and have the cell observe those notifications. See https://github/robertmryan/Carousel for an example.But having the cell update itself (rather than having the view controller repeatedly reload it, especially during the
isHighlighted
animation), avoids this animation interruption.This may be a secondary concern, but I found that the selection of the cell was more reliable when all of the cell subviews specify
isUserInteractionEnabled
offalse
(unless, of course, you need user interaction for those subviews, which is not the case at this point at least).
Some unrelated observations:
I would advise against using the
IndexPath
to identify which cell is being updated. You want to make sure that your asynchronous process will be unaffected by the insertion or deletion of cells in the collection view. That might not be something you are contemplating at this point, but using theIndexPath
is a brittle pattern that can bite you later.Needless to say, once you move the progress updates to the cell itself, you will also make sure you correctly handle the cell being reused for a different download (as the cell in question might scroll out of view and be reused).
You said:
during downloading, when the cell is clicked again, the method
didSelectItemAt
will not be called until the file is downloaded. The fact that thedidSelectItemAt
method is not called during file downloading is good.I would suggest that you really do want
didSelectItemAt
to be called whenever the cell is selected. Now, obviously, you will want to keep track of whether the download has already started or not so you don't start duplicative downloads. But do not rely on the current behavior wheredidSelectItemAt
wasn't called, as that is a mistake.Note, your view controller has a
progress
state variable. You presumably want to support multiple concurrent downloads. Thisprogress
really belongs in your model at the row level, not the view controller level. (I suspect you introduced this for the sake of the MRE, but just saying…)
DownloadItem
referenced in your code? It seems like the download-related code is critical to figuring out your problem, but you posted a WHOLE BUNCH of code. – Duncan C Commented Jan 30 at 0:50DownloadItem
at the question. I have posted the required amount of code so that you can copy it, paste it into the project and see my problem. The problem is not the download. The problem is that indidSelectItemAt
I use thisself.reloadItem(indexPath: .init(row: indexPath.row, section: 0))
. And cell start hitching when I callreloadItem(indexPath: IndexPath)
. Also If I removeself.items[indexPath.row].progress = Float(self.progress)
indidSelectItemAt
andcell.title.text = "\(String(format: "%.f%%", item.progress * 100))"
increateDataSource()
. Same result – User Commented Jan 30 at 5:32