界面旋转时全屏UICollectionViewCell重叠,动画效果异常
UICollectionView旋转时Cell重叠问题
问题概述
界面旋转时UICollectionViewCell会发生重叠,动画效果极差,完全无法用于生产环境。
代码环境
代码在iPhone 6S(NN0W2TU/A A1688,iOS 15.8.2)和iPhone 15 Pro模拟器(iOS 17)上均可复现问题。场景代理的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 } }
ViewController.swift
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 } }
补充说明
我在Stack Overflow上发现了一个类似的未解决问题。我确定这个问题有解决方案,因为切换到SwiftUI的TabView(底层基于UICollectionView)后不会出现这种糟糕的动画,但SwiftUI的TabView在界面旋转时会丢失页面索引(已知bug),该问题修复难度更高,因此无法切换方案。
更新说明
已更新问题展示视频链接。
内容的提问来源于stack exchange,提问作者Baffo rasta
相关产品推荐
相关产品推荐

