UICollectionView keep horizontal list scroll position
Asked Answered
B

4

6

I have some horizontal UICollectionViewCell in my UICollectionView.

My problem is that if I scroll the horizontal list in the first cell to another position, the fourth cell will also be at that same position.

Same for the second and the fifth, the third and the sixth...

Also don’t keep horizontal scroll position from portrait to landscape or opposite.

Is there a way for the UICollectionView in the cells to keep their position?

Update 2:

I know i have to save inner horizontal collection view content offsets in an array. I read about that in link below, but in the below link they want to achieve this in UITableView and UIScrollView. And i want to achieve this in UICollectionView inside UICollectionViewController.

Scroll View in UITableViewCell won't save position

Updated 1:

https://i.sstatic.net/ieFTe.jpg

ViewController.swift

import UIKit

class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    let cellId = "cellId"

    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView?.backgroundColor = .white
        collectionView?.register(CategoryCell.self, forCellWithReuseIdentifier: cellId)
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CategoryCell
        cell.nameLabel.text = "Horizontal list #\(indexPath.item)"
        return cell
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: view.frame.width, height: 150)
    }

}

CategoryCell.swift

import UIKit

class CategoryCell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {

    let cellId = "categoryId"

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var nameLabel: UILabel = {
        let label = UILabel()
        label.text = "Horizontal list #1"
        label.font = UIFont.systemFont(ofSize: 16)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    let appsCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.backgroundColor = .white
        return collectionView
    }()

    let dividerLineView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor(white: 0.4, alpha: 0.4)
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()

    func setupViews() {

        addSubview(appsCollectionView)
        addSubview(dividerLineView)
        addSubview(nameLabel)

        appsCollectionView.dataSource = self
        appsCollectionView.delegate = self

        appsCollectionView.register(AppCell.self, forCellWithReuseIdentifier: cellId)

        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-14-[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": nameLabel]))

        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-14-[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": dividerLineView]))

        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": appsCollectionView]))

        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[nameLabel(30)][v0][v1(0.5)]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": appsCollectionView, "v1": dividerLineView, "nameLabel": nameLabel]))

    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! AppCell
        cell.imageView.backgroundColor = UIColor(hue: CGFloat(indexPath.item) / 20.0, saturation: 0.8, brightness: 0.9, alpha: 1)
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 100, height: frame.height - 32)
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return UIEdgeInsetsMake(0, 14, 0, 14)
    }

}

AppCell.swift

import UIKit

class AppCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    let imageView: UIImageView = {
        let iv = UIImageView()
        iv.contentMode = .scaleAspectFill
        iv.layer.cornerRadius = 16
        iv.layer.masksToBounds = true
        return iv
    }()

    func setupViews() {
        addSubview(imageView)

        imageView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.width)
    }
}
Buttermilk answered 3/9, 2018 at 3:22 Comment(4)
Can you share a screenshot to explain more your problem?Candidate
Can you share what you write code for that or what constraint set to UICollectionViewCell? So we can resolve it.Choe
Thanks, Screen record and codes added.Buttermilk
Also uncheck paging enabled, otherwise UICollectionview with horizontal scrolling behaves sticky and does not stay in where scroll finished.Eatables
B
6

I finally got it :)

Thanks to 3 years old project! from irfanlone

https://github.com/irfanlone/Collection-View-in-a-collection-view-cell

It's works and support interface orientation too, but need some optimization.

For example if you scroll to the right and then scroll down fast and then scroll back to top the cell have a extra margin from the right edge! Also have some issue with interface orientation.

If anyone have a complete version please share that, thanks.

For anyone who may be interested, Add the following code in the specified files.

ViewController.swift

var storedOffsets = [Int: CGFloat]()

override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let collectionViewCell = cell as? CategoryCell else { return }
    collectionViewCell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
}

override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let collectionViewCell = cell as? CategoryCell else { return }
    storedOffsets[indexPath.row] = collectionViewCell.collectionViewOffset
}

CategoryCell.swift

var collectionViewOffset: CGFloat {
    set {
        appsCollectionView.contentOffset.x = newValue
    }

    get {
        return appsCollectionView.contentOffset.x
    }
}
Buttermilk answered 6/9, 2018 at 5:23 Comment(0)
N
2

@zahaniza thank You for answer, in addition to Your code

If You are using multiple sections the code will look like

ViewController.swift

var storedOffsets = [IndexPath: CGFloat]()

override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let collectionViewCell = cell as? CategoryCell else { return }
    collectionViewCell.collectionViewOffset = storedOffsets[indexPath] ?? 0
}

override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let collectionViewCell = cell as? CategoryCell else { return }
    storedOffsets[indexPath] = collectionViewCell.collectionViewOffset
}
Northerly answered 13/3, 2020 at 14:50 Comment(0)
C
0

Inner collection views get reused. If you want to keep the scroll position for each horizontal collection view you must keep an array of scroll positions in your UIViewController and update it when inner collection view gets scrolled. Then when reusing outer collection view cells you can reset the scroll position in collectionView(collectionView:, cellForItemAt indexPath:) method.

Your outer collection view population method should look something like this:

 override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CategoryCell
          cell.nameLabel.text = "Horizontal list #\(indexPath.item)"
          cell.scrollPosition = this.scrollPositions[indexPath.item]
          return cell
}
Commonly answered 3/9, 2018 at 15:0 Comment(4)
Thanks, but how can i update scrollPositions? In which part of code? Can you be more specific.Buttermilk
There is a UIScrollViewDelegate function 'optional func scrollViewDidScroll(_ scrollView: UIScrollView)' . Just set the delegate to viewController and that's it.Commonly
Thanks again, but your answer details don’t enough. In scrollViewDidScroll i need indexPath to update scrollPositions, but how can i access it? In UIViewController, scrollViewDidScroll just return vertical scroll, how can i access inner collection view horizontal scroll offset?Buttermilk
And If i add this func scrollViewDidScroll in inner collection view how can i access to scrollPositions array to update it? To update scrollPositions if i set scrollPositions[indexPath.item] = collectionView.contentOffset i got out of range error!Buttermilk
P
0

From the answer of #zahaniza, this code fixes his issue and works well for iPhone X :

var storedOffsets = [Int: CGFloat]()

override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let collectionViewCell = cell as? CategoryCell,
     let innerCollectionView = collectionViewCell.collectionView else { return }

    // Default collection offset is -adjustedContentInset.left and not 0
    let minOffset = -innerCollectionView.adjustedContentInset.left
    let maxOffset = innerCollectionView.contentSize.width - innnerCollectionView.frame.width

    maxOffset += innerCollectionView.adjustedContentInset.left

    if let offset = storedOffsets[indexPath.row] {
        innerCollectionView.contentOffset = CGPoint(max(minOffset, miin(maxOffset, offset.x)), 0)
    } else {
        innerCollectionView.contentOffset = CGPoint(minOffset, 0);
    }
    collectionViewCell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
}

override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let collectionViewCell = cell as? CategoryCell else { return }
    storedOffsets[indexPath.row] = collectionViewCell.collectionView.contentOffset
}
Panfish answered 16/9, 2019 at 15:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.