I have a UICollectionViewCompositionalLayout
. My app is similar to the App Store. In the real App Store, when I want to scroll the "UICollectionViewCell" with the app catalog and my finger accidentally clicks on the app icon (action button), I can scroll further without any problem. But in my app, if I do the same, I can't to continue scrolling because the button action is triggered. How to fix it?
Video to show a problem:
Cell code:
import UIKit
class MediumTableCell: UICollectionViewCell, SelfConfiguringCell {
static let reuseIdentifier: String = "MediumTableCell"
let name = UILabel()
let subtitle = UILabel()
let imageView = UIImageView()
let button = UIButton(type: .custom)
let buyButton = UIButton(type: .custom)
override init(frame: CGRect) {
super.init(frame: frame)
name.font = UIFont.preferredFont(forTextStyle: .headline)
name.textColor = .label
subtitle.font = UIFont.preferredFont(forTextStyle: .subheadline)
subtitle.textColor = .secondaryLabel
buyButton.setImage(UIImage(systemName: "icloud.and.arrow.down"), for: .normal)
button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
buyButton.setContentHuggingPriority(.defaultHigh, for: .horizontal)
button.setImage(UIImage(named: "iOS2"), for: .normal)
let innerStackView = UIStackView(arrangedSubviews: [name, subtitle])
innerStackView.axis = .vertical
let outerStackView = UIStackView(arrangedSubviews: [button, innerStackView, buyButton])
outerStackView.translatesAutoresizingMaskIntoConstraints = false
outerStackView.alignment = .center
outerStackView.spacing = 10
contentView.addSubview(outerStackView)
NSLayoutConstraint.activate([
outerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
outerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
outerStackView.topAnchor.constraint(equalTo: contentView.topAnchor)
])
}
func configure(with app: App) {
name.text = app.name
subtitle.text = app.subheading
}
required init?(coder: NSCoder) {
fatalError("Just… no")
}
}
Controller code:
class AppsViewController: UIViewController {
let sections = Bundle.main.decode([Section].self, from: "appstore.json")
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, App>?
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .systemBackground
view.addSubview(collectionView)
collectionView.register(SectionHeader.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SectionHeader.reuseIdentifier)
collectionView.register(FeaturedCell.self, forCellWithReuseIdentifier: FeaturedCell.reuseIdentifier)
collectionView.register(MediumTableCell.self, forCellWithReuseIdentifier: MediumTableCell.reuseIdentifier)
collectionView.register(SmallTableCell.self, forCellWithReuseIdentifier: SmallTableCell.reuseIdentifier)
createDataSource()
reloadData()
}
func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with app: App, for indexPath: IndexPath) -> T {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else {
fatalError("Unable to dequeue \(cellType)")
}
cell.configure(with: app)
return cell
}
func createDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, App>(collectionView: collectionView) { collectionView, indexPath, app in
switch self.sections[indexPath.section].type {
case "mediumTable":
return self.configure(MediumTableCell.self, with: app, for: indexPath)
case "smallTable":
return self.configure(SmallTableCell.self, with: app, for: indexPath)
default:
return self.configure(FeaturedCell.self, with: app, for: indexPath)
}
}
dataSource?.supplementaryViewProvider = { [weak self] collectionView, kind, indexPath in
guard let sectionHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: SectionHeader.reuseIdentifier, for: indexPath) as? SectionHeader else {
return nil
}
guard let firstApp = self?.dataSource?.itemIdentifier(for: indexPath) else { return nil }
guard let section = self?.dataSource?.snapshot().sectionIdentifier(containingItem: firstApp) else { return nil }
if section.title.isEmpty { return nil }
sectionHeader.title.text = section.title
sectionHeader.subtitle.text = section.subtitle
return sectionHeader
}
}
func reloadData() {
var snapshot = NSDiffableDataSourceSnapshot<Section, App>()
snapshot.appendSections(sections)
for section in sections {
snapshot.appendItems(section.items, toSection: section)
}
dataSource?.apply(snapshot)
}
func createCompositionalLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let section = self.sections[sectionIndex]
switch section.type {
case "mediumTable":
return self.createMediumTableSection(using: section)
case "smallTable":
return self.createSmallTableSection(using: section)
default:
return self.createFeaturedSection(using: section)
}
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 20
layout.configuration = config
return layout
}
func createFeaturedSection(using section: Section) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(350))
let layoutGroup = NSCollectionLayoutGroup.horizontal(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
return layoutSection
}
func createMediumTableSection(using section: Section) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.33))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.93), heightDimension: .fractionalWidth(0.55))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
let layoutSectionHeader = createSectionHeader()
layoutSection.boundarySupplementaryItems = [layoutSectionHeader]
return layoutSection
}
func createSmallTableSection(using section: Section) -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.2))
let layoutItem = NSCollectionLayoutItem(layoutSize: itemSize)
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 0)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(200))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
let layoutSection = NSCollectionLayoutSection(group: layoutGroup)
let layoutSectionHeader = createSectionHeader()
layoutSection.boundarySupplementaryItems = [layoutSectionHeader]
return layoutSection
}
func createSectionHeader() -> NSCollectionLayoutBoundarySupplementaryItem {
let layoutSectionHeaderSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.93), heightDimension: .estimated(80))
let layoutSectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: layoutSectionHeaderSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
return layoutSectionHeader
}
}
Update:
I add button in CollevtionView cell to show animation when user click on it. But no one found a way to fix my problem described above. I found 2 ways to solve the problem.
First:
I remove button and add this code to show animation. This code fix scrolling problem described above and show animation on cell touch.
Cell code:
import UIKit
class MediumTableCell: UICollectionViewCell, SelfConfiguringCell {
static let reuseIdentifier: String = "MediumTableCell"
var highlightedScale: CGFloat = 0.9
var scaleDownDuration: TimeInterval = 0.4
var scaleUpDuration: TimeInterval = 0.38
override var isHighlighted: Bool {
didSet {
if oldValue == false && isHighlighted {
highlight() }
else if oldValue == true && !isHighlighted {
unHighlight()
}
}
}
private func animateScale(to scale: CGFloat, duration: TimeInterval) {
UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [], animations: {
self.stackView.transform = .init(scaleX: scale, y: scale)
self.textView.transform = .init(scaleX: scale, y: scale)
}, completion: nil)
}
func highlight() {
backgroundColor = .white
animateScale(to: highlightedScale, duration: scaleDownDuration)
}
func unHighlight() {
backgroundColor = .white.withAlphaComponent(0.88)
animateScale(to: 1, duration: scaleUpDuration)
isSelected.toggle()
}
}
But in this case sometimes I see spontaneous cell animation when I scroll the CollectionView
and don’t click on the cell. How to fix it?
Second:
Controller code:
class AppsViewController: UIViewController, UICollectionViewDelegate {
collectionView.delegate = self
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MediumTableCell.reuseIdentifier, for: indexPath) as! MediumTableCell
UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.5, options: [], animations: {
cell.stackView.transform = CGAffineTransform(scaleX: scale, y: scale)
cell.textView.transform = CGAffineTransform(scaleX: scale, y: scale)
}, completion: nil)
let index = indexPath.item
print("\(index)")
}
In this case I can see right indexPath.item when I click on cell but animation doesn't work.
This line don't call cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MediumTableCell.reuseIdentifier, for: indexPath) as! MediumTableCell
Why?
All the events are there on the cell contentView & collection view.
As you have UIButton above contentView, it responds to that first and hence UIButton click event will get called.
You should open popup for the app details using didSelectRow event and not UIButton.
Change UIButton to UIImageView (believe there will be image instead of 2) or change to something else as per requirement.