简体   繁体   English

Swift - 按单元分页 UICollectionView 同时保持单元格水平居中

[英]Swift - Paging UICollectionView by cell while keeping the cell horizontally centered

First, not sure if the title is correct or offers the best description but I'm not sure what else to use.首先,不确定标题是否正确或提供了最好的描述,但我不确定还可以使用什么。

So, i'm working on an app and I reached a section where I got stuck while implementing the UI.所以,我正在开发一个应用程序,但我在实现 UI 时遇到了卡住的部分。 Basically, I have a VC (image below) that can segue to its self based on the info I get from a JSON file.基本上,我有一个 VC(下图),它可以根据我从 JSON 文件中获得的信息进行自我定位。

在此处输入图片说明

The thing is I need to have a carousel-like menu in the upper side with an undefined number of cells (again, depends on what I get from the JSON file).问题是我需要在上侧有一个类似轮播的菜单,其中包含未定义数量的单元格(同样,取决于我从 JSON 文件中获得的内容)。 I decided to go for a UICollectionView for this and I managed to implement the basics without any problem.我决定为此使用 UICollectionView 并且我设法毫无问题地实现了基础知识。

But here is the part where I got stuck:但这是我卡住的部分:

  1. Since the selected cell must ALWAYS be centered, when the first and the last cell gets selected, I need to have an empty space between the cell and the safe area (see the image above).由于选定的单元格必须始终居中,当第一个和最后一个单元格被选中时,我需要在单元格和安全区域之间留出一个空白区域(见上图)。
  2. The scroll needs to be paged.滚动需要分页。 Normally this wouldn't be a problem if the UICollectionView cell would have a width almost equal to the one of the screen but the requirement is to be able to scroll one element at a time (see second screen above).通常,如果 UICollectionView 单元格的宽度几乎等于屏幕之一的宽度,这不会成为问题,但要求是能够一次滚动一个元素(参见上面的第二个屏幕)。

I tried finding something similar but maybe I'm not looking for the right thing because all I could find was Paging UICollectionView by cells, not screen我试图找到类似的东西,但也许我没有在寻找正确的东西,因为我能找到的只是按单元格而不是屏幕分页 UICollectionView

Also, to be honest I've never seen an app / UICollectionView with this behaviour.另外,老实说,我从未见过具有这种行为的应用程序/ UICollectionView。

I posted parts of the code below but it's not really gonna help much since it's just standard UICollectionView methods.我在下面发布了部分代码,但它并没有多大帮助,因为它只是标准的 UICollectionView 方法。

Any suggestions?有什么建议?

class PreSignupDataVC : UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIPickerViewDelegate, UIPickerViewDataSource

@IBOutlet weak var cvQuestions: UICollectionView!

var questionCell : PreSignupDataQuestionCellVC!
var screenData : Array<PreSignupScreenData> = Array<PreSignupScreenData>()
var pvDataSource : [String] = []
var numberOfComponents : Int = 0
var numberOfRowsInComponent : Int = 0
var currentScreen : Int = 1
var selectedType : Int?
var selectedCell : Int = 0
var initialLastCellInsetPoint : CGFloat = 0.0

override func viewDidLoad()
{
    super.viewDidLoad()

    print("PreSignupDataVC > viewDidLoad")

    initialLastCellInsetPoint = (self.view.frame.width - 170)/2
    screenData = DataSingleton.sharedInstance.returnPreSignUpUIArray()[selectedType!].screenData
    numberOfComponents = screenData[currentScreen - 1].controls[0].numberOfComponents!
    numberOfRowsInComponent = screenData[currentScreen - 1].controls[0].controlDataSource.count
    pvDataSource = screenData[currentScreen - 1].controls[0].controlDataSource

    cvQuestions.register(UINib(nibName: "PreSignupDataQuestionCell",
                               bundle: nil),
                         forCellWithReuseIdentifier: "PreSignupDataQuestionCellVC")
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
    print("PreSignupDataVC > collectionView > numberOfItemsInSection")

    return screenData[currentScreen - 1].controls.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
    print("PreSignupDataVC > collectionView > cellForItemAt")

    questionCell = (cvQuestions.dequeueReusableCell(withReuseIdentifier: "PreSignupDataQuestionCellVC",
                                                    for: indexPath) as? PreSignupDataQuestionCellVC)!
    questionCell.vQuestionCellCellContainer.layer.cornerRadius = 8.0
    questionCell.lblQuestion.text = screenData[currentScreen - 1].controls[indexPath.row].cellTitle
    questionCell.ivQuestionCellImage.image = UIImage(named: screenData[currentScreen - 1].controls[indexPath.row].cellUnselectedIcon!)

    return questionCell
}

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
    print("PreSignupDataVC > collectionView > didSelectItemAt")

    numberOfComponents = screenData[currentScreen - 1].controls[indexPath.row].numberOfComponents!
    numberOfRowsInComponent = screenData[currentScreen - 1].controls[indexPath.row].controlDataSource.count
    pvDataSource = screenData[currentScreen - 1].controls[indexPath.row].controlDataSource
    selectedCell = indexPath.row

    pvData.reloadAllComponents()
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
    print("PreSignupDataVC > collectionView > insetForSectionAt")

    return UIEdgeInsets(top: 0.0, left: initialLastCellInsetPoint, bottom: 00.0, right: initialLastCellInsetPoint)
}

#1. #1. Using UICollectionViewCompositionalLayout (requires iOS 13)使用UICollectionViewCompositionalLayout (需要 iOS 13)

Since iOS 13, you can use UICollectionViewCompositionalLayout and set NSCollectionLayoutSection 's orthogonalScrollingBehavior property to .groupPagingCentered in order to have a centered horizontal carousel-like layout.从 iOS 13 开始,您可以使用UICollectionViewCompositionalLayout并将NSCollectionLayoutSectionorthogonalScrollingBehavior .groupPagingCentered属性设置为.groupPagingCentered以获得居中的水平轮播式布局。

The following Swift 5.1 sample code shows a possible implementation of UICollectionViewCompositionalLayout in order to have the layout you want:以下 Swift 5.1 示例代码显示了UICollectionViewCompositionalLayout的可能实现,以便获得您想要的布局:

CollectionView.swift CollectionView.swift

import UIKit

class CollectionView: UICollectionView {

    override var safeAreaInsets: UIEdgeInsets {
        return UIEdgeInsets(top: super.safeAreaInsets.top, left: 0, bottom: super.safeAreaInsets.bottom, right: 0)
    }

}

ViewController.swift视图控制器.swift

import UIKit

class ViewController: UIViewController {

    var collectionView: CollectionView!
    var dataSource: UICollectionViewDiffableDataSource<Int, Int>!

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Collection view"

        // Compositional layout

        let layout = UICollectionViewCompositionalLayout(sectionProvider: {
            (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in

            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)

            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalHeight(1), heightDimension: .fractionalHeight(1))
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

            let section = NSCollectionLayoutSection(group: group)
            section.orthogonalScrollingBehavior = UICollectionLayoutSectionOrthogonalScrollingBehavior.groupPagingCentered

            return section
        })

        // Set collection view

        collectionView = CollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .systemGroupedBackground
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")

        // View layout

        view.addSubview(collectionView)

        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.heightAnchor.constraint(equalToConstant: 160).isActive = true
        collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true

        // Collection view diffable data source

        dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView, cellProvider: {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in

            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
            return cell
        })

        var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
        snapshot.appendSections([0])
        snapshot.appendItems(Array(0 ..< 5))
        dataSource.apply(snapshot, animatingDifferences: false)
    }

}

Cell.swift Cell.swift

import UIKit

class Cell: UICollectionViewCell {

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

        contentView.backgroundColor = .orange
    }

    required init?(coder: NSCoder) {
        fatalError("not implemnted")
    }

}

#2. #2. Using UICollectionViewFlowLayout使用UICollectionViewFlowLayout

If you're targeting a lower version of iOS than iOS 13, you can subclass UICollectionViewFlowLayout , compute the horizontal insets in prepare() and implement targetContentOffset(forProposedContentOffset:withScrollingVelocity:) in order to force the cell to be centered after user scroll.如果您的目标是低于 iOS 13 的 iOS 版本,您可以UICollectionViewFlowLayout ,在prepare()计算水平插入并实现targetContentOffset(forProposedContentOffset:withScrollingVelocity:)以强制单元格在用户滚动后居中。

The following Swift 5.1 sample code shows how to implement the subclass of UICollectionViewFlowLayout :以下 Swift 5.1 示例代码展示了如何实现UICollectionViewFlowLayout的子类:

ViewController.swift视图控制器.swift

import UIKit

class ViewController: UIViewController {

    let flowLayout = PaggedFlowLayout()

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Collection view"

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
        collectionView.backgroundColor = .systemGroupedBackground
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.decelerationRate = .fast
        collectionView.dataSource = self
        collectionView.contentInsetAdjustmentBehavior = .never
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")

        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.heightAnchor.constraint(equalToConstant: 160).isActive = true
        collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    }

}
extension ViewController: UICollectionViewDataSource {

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        return cell
    }

}

PaggedFlowLayout.swift PaggedFlowLayout.swift

import UIKit

class PaggedFlowLayout: UICollectionViewFlowLayout {

    override init() {
        super.init()

        scrollDirection = .horizontal
        minimumLineSpacing = 5
        minimumInteritemSpacing = 0
        sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
    }

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

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { fatalError() }

        // itemSize
        let itemHeight = collectionView.bounds.height - sectionInset.top - sectionInset.bottom
        itemSize = CGSize(width: itemHeight, height: itemHeight)

        // horizontal insets
        let horizontalInsets = (collectionView.bounds.width - itemSize.width) / 2
        sectionInset.left = horizontalInsets
        sectionInset.right = horizontalInsets
    }

    /*
     Add some snapping behaviour to center the cell after scrolling.
     Source: https://stackoverflow.com/a/14291208/1966109
     */
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else { return .zero }

        var proposedContentOffset = proposedContentOffset
        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalCenter = proposedContentOffset.x + collectionView.bounds.size.width / 2
        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)

        guard let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) else { return .zero }

        for layoutAttributes in layoutAttributesArray {
            let itemHorizontalCenter = layoutAttributes.center.x
            if abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjustment) {
                offsetAdjustment = itemHorizontalCenter - horizontalCenter
            }
        }

        var nextOffset = proposedContentOffset.x + offsetAdjustment
        let snapStep = itemSize.width + minimumLineSpacing

        func isValidOffset(_ offset: CGFloat) -> Bool {
            let minContentOffset = -collectionView.contentInset.left
            let maxContentOffset = collectionView.contentInset.left + collectionView.contentSize.width - itemSize.width
            return offset >= minContentOffset && offset <= maxContentOffset
        }

        repeat {
            proposedContentOffset.x = nextOffset
            let deltaX = proposedContentOffset.x - collectionView.contentOffset.x
            let velX = velocity.x

            if deltaX.sign.rawValue * velX.sign.rawValue != -1 {
                break
            }

            nextOffset += CGFloat(velocity.x.sign.rawValue) * snapStep
        } while isValidOffset(nextOffset)

        return proposedContentOffset
    }

}

Cell.swift Cell.swift

import UIKit

class Cell: UICollectionViewCell {

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

        contentView.backgroundColor = .orange
    }

    required init?(coder: NSCoder) {
        fatalError("not implemnted")
    }

}

Display on iPhone 11 Pro Max: iPhone 11 Pro Max 上的显示:

在此处输入图片说明

You can use insetForSectionAtIndex to add spacing at first and last cell 您可以使用 insetForSectionAtIndex在第一个和最后一个单元格添加间距

UPDATE更新
You can use scroll view.您可以使用滚动视图。
First: add leading and trailing to scroll view:首先:添加前导和尾随滚动视图:
在此处输入图片说明

Second: scrollView.clipsToBounds = false第二: scrollView.clipsToBounds = false

Third: add view to scroll view第三:添加视图滚动视图

func setupScrollView() {
    DispatchQueue.main.async {
        self.scrollView.layoutIfNeeded() // in order to get correct frame

        let numberItem = 6
        let spaceBetweenView: CGFloat = 20
        let firstAndLastSpace: CGFloat = spaceBetweenView / 2            
        let width = self.scrollView.frame.size.width
        let height = self.scrollView.frame.size.height
        let numberOfView: CGFloat = CGFloat(numberItem)
        let scrollViewContentSizeWidth = numberOfView * width

        self.scrollView.contentSize = CGSize(width: scrollViewContentSizeWidth, height: height)

        for index in 0..<numberItem {
            let xCoordinate = (CGFloat(index) * (width)) + firstAndLastSpace
            let viewFrame = CGRect(x: xCoordinate, y: 0, width: width - spaceBetweenView, height: height)
            let view = UIView()
            view.backgroundColor = .red

            view.frame = viewFrame
            self.scrollView.addSubview(view)
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM