swiftuicollectionviewuicollectionviewcompositionallayout

Swift Compositional Layout Expanding Cell Causes Unwanted Offset Scroll Behavior


I'm wanting on creating expanding cells using Compositional Layout. An issue i'm facing is that every time when my cell expands, i'm getting this unwanted offset change occurring right after my expansion is complete (moving cells to the right). For the life of me, im not understanding why its scrolling. Looking at the expansion state, its not like there were wasnt enough room on the left side of the screen. So im unsure of what's causing this and if we can manually turn it off. Any suggestions would be appreciated!

class ViewController: UIViewController, UICollectionViewDelegate {

lazy var collectionView: UICollectionView = {
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
    collectionView.translatesAutoresizingMaskIntoConstraints = false
    collectionView.delegate = self
    collectionView.contentInsetAdjustmentBehavior = .never
    collectionView.alwaysBounceVertical = true
    collectionView.alwaysBounceHorizontal = true
    return collectionView
}()

var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var items = [Item]()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(collectionView)
    setupCollectionViewDataSource()
    ...
}
   func setupCollectionViewDataSource() {
    dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, itemIdentifier in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ProminentCell.cellId, for: indexPath) as! ProminentCell
        cell.backgroundColor = self.colors.randomElement()
        cell.numberLabel.text = String(indexPath.item)
        cell.delegate = self
        return cell
    })
    
    updateDataSource()
}

func createLayout() -> UICollectionViewLayout {
    let layout = UICollectionViewCompositionalLayout { sectionNumber, env in
        let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(325), heightDimension: .absolute(708))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
        
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .continuous
        
        section.interGroupSpacing = 10
        return section
    }
    return layout
}

private func updateDataSource() {
    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
    snapshot.appendSections([.carousel])
    snapshot.appendItems(items)
    dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    if let cell = collectionView.cellForItem(at: indexPath) as? ProminentCell {
        cell.currentState = .expanded
    }
}

func collectionView(_ collectionView: UICollectionView, didUpdateFocusIn context: UICollectionViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
    guard let nextIndexPath = context.nextFocusedIndexPath, let prevIndexPath = context.previouslyFocusedIndexPath else { return }
    collectionView.isScrollEnabled = false
    collectionView.scrollToItem(at: nextIndexPath, at: .left, animated: true)
  }
}

extension ViewController: ProminentCellDelegate {
  func expandCell(cell: ProminentCell) {
    let snapshot = dataSource.snapshot()
    dataSource.apply(snapshot, animatingDifferences: true, completion: nil)
  }
}

Cell Class

protocol ProminentCellDelegate: AnyObject {
    func expandCell(cell: ProminentCell)
    func collapseCell(cell: ProminentCell)
}

enum ProminentCellState {
    case collapsed, expanded
}

class ProminentCell: UICollectionViewCell {

 static let cellId = "cellId"
 weak var delegate: ProminentCellDelegate?

 var currentState: ProminentCellState {
     didSet {
         if currentState == .collapsed {
             collapseCell {}
         } else {
             expandCell(completion: {})
         }
     }
 }

 let imageStringName = ""

 lazy var backgroundImageView: UIImageView = {
    let imageView = UIImageView(image: UIImage(named: imageStringName))
    imageView.backgroundColor = .systemOrange
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.adjustsImageWhenAncestorFocused = true
    return imageView
}()

let numberLabel: UILabel = {
    let label = UILabel()
    ...
    return label
}()

private let expandedWidth = 1035.0
private let collapsedWidth = 325.0
private let animationDuration = 0.33

var closedConstraint: NSLayoutConstraint? = nil
var openConstraint: NSLayoutConstraint? = nil

override init(frame: CGRect) {
    currentState = .collapsed
    super.init(frame: frame)
    
    contentView.addSubview(backgroundImageView)
    contentView.addSubview(numberLabel)
    contentView.translatesAutoresizingMaskIntoConstraints = false

    let padding = 10.0
    NSLayoutConstraint.activate([
        contentView.topAnchor.constraint(equalTo: topAnchor),
        contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
        contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
        contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
        backgroundImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
        backgroundImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
        backgroundImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
        backgroundImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding),
        
        numberLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
        numberLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
    ])
    
    closedConstraint = contentView.widthAnchor.constraint(equalToConstant: collapsedWidth)
    closedConstraint?.isActive = true
    openConstraint = contentView.widthAnchor.constraint(equalToConstant: expandedWidth)
}

private func expandCell(completion: () -> Void) {
    closedConstraint?.isActive = false
    openConstraint?.isActive = true
    
    let curFrame = frame.origin

    let expandedSize = CGRect(x: curFrame.x, y: curFrame.y, width: expandedWidth, height: frame.size.height)
    UIViewPropertyAnimator.runningPropertyAnimator(withDuration: animationDuration, delay: 0, options: [.curveLinear]) {
        self.frame = expandedSize
        self.layoutIfNeeded()
        self.contentView.layoutIfNeeded()
    }
    delegate?.expandCell(cell: self)
}

private func collapseCell(completion: () -> Void) {
    openConstraint?.isActive = false
    closedConstraint?.isActive = true

    let curFrame = frame.origin
    let collapsedSize = CGRect(x: curFrame.x, y: curFrame.y, width: collapsedWidth, height: frame.size.height)

    UIViewPropertyAnimator.runningPropertyAnimator(withDuration: animationDuration, delay: 0, options: [.beginFromCurrentState]) {
        self.frame = collapsedSize
        self.contentView.layoutIfNeeded()
        self.layoutIfNeeded()
    }
    
    delegate?.collapseCell(cell: self)
}
...

}

enter image description here


Solution

  • The solution was to turn the orthogonal scrollView isScrollEnabled to false. This removes the native scroll animation that occurs when scrollView's content size changes.