全宽 UICollectionViewCells 在界面方向旋转期间重叠,导致丑陋的动画

问题描述 投票:0回答:1

前言

我将在序言中直观地说明这个问题。 这里是该问题的视频:旋转界面时,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),这可能更难以解决。

swift uicollectionview uikit uicollectionviewlayout
1个回答
0
投票

尚不完全清楚您遇到的情况,但请尝试将其添加到您的

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
        }
    )
}

看看是否可以解决问题。

© www.soinside.com 2019 - 2024. All rights reserved.