Preface
Upon rotating the interface, the UICollectionViewCells overlap, generating an unpleasant animation that for sure can't be used in production.
The code
The code was executed on iPhone 6S (NN0W2TU/A A1688) with iOS 15.8.2. I could reproduce the issue on iPhone 15 Pro with iOS 17 on simulator as well.
SelfConfiguringCell.swift:
import UIKit
protocol SelfConfiguringCell: UICollectionViewCell {
static var reuseIdentifier: String { get }
func configure(with image: String)
}
ISVImageScrollView.swift: Code here
CarouselCell.swift:
import UIKit
import SnapKit
class CarouselCell: UICollectionViewCell, SelfConfiguringCell, UIScrollViewDelegate {
static var reuseIdentifier: String = "carousel.cell"
internal var image: String = "placeholder" {
didSet {
self.imageView = UIImageView(image: UIImage(named: image))
self.scrollView.imageView = self.imageView
}
}
let scrollView: ISVImageScrollView = {
let scrollView = ISVImageScrollView()
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 30.0
scrollView.zoomScale = 1.0
scrollView.contentOffset = .zero
scrollView.bouncesZoom = true
return scrollView
}()
var imageView: UIImageView = {
let image = UIImage(named: "placeholder")!
let imageView = UIImageView(image: image)
return imageView
}()
func setImage(_ image: String) {
self.image = image
}
func configure(with image: String) {
self.setImage(image)
self.scrollView.snp.makeConstraints { make in
make.left.top.right.bottom.equalTo(contentView)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = UIColor.black
scrollView.delegate = self
scrollView.imageView = self.imageView
contentView.addSubview(scrollView)
}
required init?(coder: NSCoder) {
fatalError("Cannot init from storyboard")
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.imageView
}
}
ViewController:
import UIKit
class ViewController: UICollectionViewController {
var currentPage: IndexPath? = nil
let images = ["police", "shutters", "depot", "cakes", "sign"]
init() {
let compositionalLayout = UICollectionViewCompositionalLayout { sectionIndex, environment in
let absoluteW = environment.container.effectiveContentSize.width
let absoluteH = environment.container.effectiveContentSize.height
// Handle landscape
if absoluteW > absoluteH {
print("landscape")
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return section
} else {
// Handle portrait
print("portrait")
let itemSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(absoluteW * 9.0/16.0)
)
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(absoluteW * 9.0/16.0)
)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
return section
}
}
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 0
config.scrollDirection = .horizontal
compositionalLayout.configuration = config
super.init(collectionViewLayout: compositionalLayout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isPagingEnabled = true
// Register cell for reuse
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.images.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let reusableCell = collectionView.dequeueReusableCell(withReuseIdentifier: CarouselCell.reuseIdentifier, for: indexPath) as? CarouselCell else {
fatalError()
}
let index : Int = (indexPath.section * self.images.count) + indexPath.row
reusableCell.configure(with: self.images[index])
return reusableCell
}
}
Notes
I found a similar unanswered question here. I'm sure something can be done about it because if I switch to SwiftUI with a TabView
, that according to SwiftUI Introspect documentation for TabViewWithPageStyleType, is using UICollectionView
under the hood, I'm not getting that ugly animation anymore. Though I can't switch to SwiftUI to use TabView
because on interface rotation it loses the page index (well known bug, see here), which probably is even trickier to workaround.