怠慢プログラマーの備忘録

怠慢でナマケモノなプログラマーの備忘録です。

【iOS】prev/nextのviewを表示させるHorizontalScroll(FlowLayout)[備忘録]

HorizontalScroll

上記画像のような両端(prev/next)の要素(view)が見えてる状態で、かつAndroidのsnapHelperが効いている(スクロールする際に1番表示領域の大きい要素を中央へスクロールされる)ようなHorizontalScrollの実装方法です。

本来両端の要素を表示しない場合は padding enabledを使用すれば実現できますが、両端のprev/nextのviewを表示しながらの場合はpadding enabledでは実現できません。

基本的にCollectionViewを用いて実装します。

※collectionviewの基本的な設定やcollectionviewcellは省略します。
※そして雑に書きます。
Androidはこちらです。

yutaabe200.hatenablog.com

FlowLayout.swift

import UIKit

final class FlowLayout: UICollectionViewFlowLayout {

    private var layoutAttributesForPaging: [UICollectionViewLayoutAttributes]?

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        guard let collectionView = collectionView else { return proposedContentOffset }
        guard let targetAttributes = layoutAttributesForPaging else { return proposedContentOffset }

        let nextAttributes: UICollectionViewLayoutAttributes?
        if velocity.x == 0 {
            nextAttributes = layoutAttributesForNearbyCenterX(in: targetAttributes, collectionView: collectionView)
        } else if velocity.x > 0 {
            nextAttributes = targetAttributes.last
        } else {
            nextAttributes = targetAttributes.first
        }
        guard let attributes = nextAttributes else { return proposedContentOffset }

        if attributes.representedElementKind == UICollectionView.elementKindSectionHeader {
            return CGPoint(x: 0, y: collectionView.contentOffset.y)
        } else {
            let cellLeftMargin = (collectionView.bounds.width - attributes.bounds.width) * 0.5
            return CGPoint(x: attributes.frame.minX - cellLeftMargin, y: collectionView.contentOffset.y)
        }
    }
    
    private func layoutAttributesForNearbyCenterX(in attributes: [UICollectionViewLayoutAttributes], collectionView: UICollectionView) -> UICollectionViewLayoutAttributes? {
        let screenCenterX = collectionView.contentOffset.x + collectionView.bounds.width * 0.5
        let result = attributes.reduce((attributes: nil as UICollectionViewLayoutAttributes?, distance: CGFloat.infinity)) { result, attributes in
            let distance = attributes.frame.midX - screenCenterX
            return abs(distance) < abs(result.distance) ? (attributes, distance) : result
        }
        return result.attributes
    }
    
    func prepareForPaging() {        
        guard let collectionView = collectionView else { return }
        let expansionMargin = sectionInset.left + sectionInset.right
        let expandedVisibleRect = CGRect(x: collectionView.contentOffset.x - expansionMargin,
                                         y: 0,
                                         width: collectionView.bounds.width + (expansionMargin * 2),
                                         height: collectionView.bounds.height)
        layoutAttributesForPaging = layoutAttributesForElements(in: expandedVisibleRect)?.sorted { $0.frame.minX < $1.frame.minX }
    }
}

ViewController.swift

import UIKit

final class ViewController: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.delegate = self
            collectionView.dataSource = self
            collectionView
                .register(
                    UINib(nibName: "CollectionViewCell", bundle: nil),
                    forCellWithReuseIdentifier: "CollectionViewCell"
            )
            flowLayout.scrollDirection = .horizontal
            flowLayout.minimumLineSpacing = 0
            flowLayout.sectionInset = UIEdgeInsets(top: 0, left: 4, bottom: 0, right: 4)
            collectionView.collectionViewLayout = flowLayout
            collectionView.decelerationRate = .fast
        }
    }
    
    private lazy var flowLayout = FlowLayout()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}

// MARK: UICollectionViewDelegateFlowLayout
extension ViewController: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(
            width: self.collectionView.bounds.width * 0.7,
            height: self.collectionView.bounds.height * 0.7
        )
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        self.flowLayout.prepareForPaging()
    }
    
}

// MARK: UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 3
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell: CollectionViewCell =
            self.collectionView.dequeueReusableCell(
                withReuseIdentifier: "CollectionViewCell",
                for: indexPath
            ) as? CollectionViewCell
            else { return  UICollectionViewCell() }
        return cell
    }
    
}

実行すると以下のようになります。

scaleを変更する

次に中央に表示されている要素を拡大表示させる場合です。 f:id:ka0in:20200525014302p:plain

   private func transformScale(cell: UICollectionViewCell) {
        let cellCenter: CGPoint = self.collectionView.convert(cell.center, to: nil)
        let screenCenterX: CGFloat = UIScreen.main.bounds.width / 2
        let reductionRatio: CGFloat = -0.0005
        let maxScale: CGFloat = 1
        let cellCenterDisX: CGFloat = abs(screenCenterX - cellCenter.x)
        let newScale = reductionRatio * cellCenterDisX + maxScale
        cell.transform = CGAffineTransform(scaleX: newScale, y: newScale)
    }

これをviewWillLayoutSubviews()UICollectionViewDelegateFlowLayoutscrollViewDidScroll()で、visibleCellsのcellを渡してあげる感じです。

        self.collectionView.visibleCells.forEach { cell in
            self.transformScale(cell: cell)
        }

今回はCollectionViewでやりましたが、ScrollView+StackViewでもできるかと思います。

sampleはこちら↓

github.com