我将在序言中直观地说明这个问题。 这里是该问题的视频:旋转界面时,UICollectionViewCells 重叠,生成令人不快的动画,肯定无法在生产中使用。
代码在运行 iOS 15.8.2 的 iPhone 6S (NN0W2TU/A A1688) 上执行。我也可以在模拟器上使用 iOS 17 的 iPhone 15 Pro 重现该问题。
请注意我的场景代表的
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { }
集 window?.rootViewController = ViewController()
。
SelfConfiguringCell.swift:
import UIKit
protocol SelfConfiguringCell: UICollectionViewCell {
static var reuseIdentifier: String { get }
func configure(with image: String)
}
ISVImageScrollView.swift:
import UIKit
public class ISVImageScrollView: UIScrollView, UIGestureRecognizerDelegate {
// MARK: - Public
public var imageView: UIImageView? {
didSet {
oldValue?.removeGestureRecognizer(self.tap)
oldValue?.removeFromSuperview()
if let imageView = self.imageView {
self.initialImageFrame = .null
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(self.tap)
self.addSubview(imageView)
}
}
}
// MARK: - Initialization
override init(frame: CGRect) {
super.init(frame: frame)
self.configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.configure()
}
deinit {
self.stopObservingBoundsChange()
}
// MARK: - UIScrollView
public override func layoutSubviews() {
super.layoutSubviews()
self.setupInitialImageFrame()
}
public override var contentOffset: CGPoint {
didSet {
let contentSize = self.contentSize
let scrollViewSize = self.bounds.size
var newContentOffset = contentOffset
if contentSize.width < scrollViewSize.width {
newContentOffset.x = (contentSize.width - scrollViewSize.width) * 0.5
}
if contentSize.height < scrollViewSize.height {
newContentOffset.y = (contentSize.height - scrollViewSize.height) * 0.5
}
super.contentOffset = newContentOffset
}
}
// MARK: - UIGestureRecognizerDelegate
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return otherGestureRecognizer === self.panGestureRecognizer
}
// MARK: - Private: Tap to Zoom
private lazy var tap: UITapGestureRecognizer = {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapToZoom(_:)))
tap.numberOfTapsRequired = 2
tap.delegate = self
return tap
}()
@IBAction private func tapToZoom(_ sender: UIGestureRecognizer) {
guard sender.state == .ended else { return }
if self.zoomScale > self.minimumZoomScale {
self.setZoomScale(self.minimumZoomScale, animated: true)
} else {
guard let imageView = self.imageView else { return }
let tapLocation = sender.location(in: imageView)
let zoomRectWidth = imageView.frame.size.width / self.maximumZoomScale;
let zoomRectHeight = imageView.frame.size.height / self.maximumZoomScale;
let zoomRectX = tapLocation.x - zoomRectWidth * 0.5;
let zoomRectY = tapLocation.y - zoomRectHeight * 0.5;
let zoomRect = CGRect(
x: zoomRectX,
y: zoomRectY,
width: zoomRectWidth,
height: zoomRectHeight)
self.zoom(to: zoomRect, animated: true)
}
}
// MARK: - Private: Geometry
private var initialImageFrame: CGRect = .null
private var imageAspectRatio: CGFloat {
guard let image = self.imageView?.image else { return 1 }
return image.size.width / image.size.height
}
private func configure() {
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.startObservingBoundsChange()
}
private func rectSize(for aspectRatio: CGFloat, thatFits size: CGSize) -> CGSize {
let containerWidth = size.width
let containerHeight = size.height
var resultWidth: CGFloat = 0
var resultHeight: CGFloat = 0
if aspectRatio <= 0 || containerHeight <= 0 {
return size
}
if containerWidth / containerHeight >= aspectRatio {
resultHeight = containerHeight
resultWidth = containerHeight * aspectRatio
} else {
resultWidth = containerWidth
resultHeight = containerWidth / aspectRatio
}
return CGSize(width: resultWidth, height: resultHeight)
}
private func scaleImageForTransition(from oldBounds: CGRect, to newBounds: CGRect) {
guard let imageView = self.imageView else { return}
let oldContentOffset = CGPoint(x: oldBounds.origin.x, y: oldBounds.origin.y)
let oldSize = oldBounds.size
let newSize = newBounds.size
var containedImageSizeOld = self.rectSize(for: self.imageAspectRatio, thatFits: oldSize)
let containedImageSizeNew = self.rectSize(for: self.imageAspectRatio, thatFits: newSize)
if containedImageSizeOld.height <= 0 {
containedImageSizeOld = containedImageSizeNew
}
let orientationRatio = containedImageSizeNew.height / containedImageSizeOld.height
let transform = CGAffineTransform(scaleX: orientationRatio, y: orientationRatio)
self.imageView?.frame = imageView.frame.applying(transform)
self.contentSize = imageView.frame.size;
var xOffset = (oldContentOffset.x + oldSize.width * 0.5) * orientationRatio - newSize.width * 0.5
var yOffset = (oldContentOffset.y + oldSize.height * 0.5) * orientationRatio - newSize.height * 0.5
xOffset -= max(xOffset + newSize.width - self.contentSize.width, 0)
yOffset -= max(yOffset + newSize.height - self.contentSize.height, 0)
xOffset -= min(xOffset, 0)
yOffset -= min(yOffset, 0)
self.contentOffset = CGPoint(x: xOffset, y: yOffset)
}
private func setupInitialImageFrame() {
guard self.imageView != nil, self.initialImageFrame == .null else { return }
let imageViewSize = self.rectSize(for: self.imageAspectRatio, thatFits: self.bounds.size)
self.initialImageFrame = CGRect(x: 0, y: 0, width: imageViewSize.width, height: imageViewSize.height)
self.imageView?.frame = self.initialImageFrame
self.contentSize = self.initialImageFrame.size
}
// MARK: - Private: KVO
private var boundsObserver: NSKeyValueObservation?
private func startObservingBoundsChange() {
self.boundsObserver = self.observe(
\.self.bounds,
options: [.old, .new],
changeHandler: { [weak self] (object, change) in
if let oldRect = change.oldValue,
let newRect = change.newValue,
oldRect.size != newRect.size {
self?.scaleImageForTransition(from: oldRect, to: newRect)
}
})
}
private func stopObservingBoundsChange() {
self.boundsObserver?.invalidate()
self.boundsObserver = nil
}
}
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
}
}
fileprivate 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
}()
fileprivate var imageView: UIImageView = {
let image = UIImage(named: "placeholder")!
let imageView = UIImageView(image: image)
return imageView
}()
public 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
}
}
视图控制器:
import UIKit
class ViewController: UICollectionViewController {
private var currentPage: IndexPath? = nil
private 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
}
}
我发现了一个类似的未解答的问题这里。我确信可以对此采取一些措施,因为如果我使用
TabView
切换到 SwiftUI,根据 this,在引擎盖下使用 UICollectionView
,我就不会再看到丑陋的动画了。虽然我无法切换到 SwiftUI 来使用 TabView
,因为在界面旋转时它会丢失页面索引(众所周知的错误,请参见here),这可能更难以解决。
尚不完全清楚您遇到的情况,但请尝试将其添加到您的
ViewController
类中:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let theLastIndex = Int(self.collectionView.contentOffset.x / self.collectionView.bounds.width)
coordinator.animate(
alongsideTransition: { [unowned self] _ in
self.collectionView.scrollToItem(at: IndexPath(item: theLastIndex, section: 0), at: .centeredHorizontally, animated: true)
},
completion: { [unowned self] _ in
// if we want to do something after the size transition
}
)
}
看看是否可以解决问题。