티스토리 뷰

보통 회전목마 라고 많이 하는데 수평방향으로 스크롤되는 뷰이다.

스크롤뷰를 사용해서 구현하는 방법, 컬렉션뷰를 이용해서 구현하는 방법이 있는데

이번에는 컬렉션뷰를 이용해서 구현해보겠다.

모든 앱에서 흔히 쓰이는 기능이라 알아두면 좋을 것 같다!

 

순서를 차근차근 설명해보겠다 !

내가 만드려고 하는 뷰는 요런 뷰이다!

가운데뷰 양옆 뷰에 알파값, 사이즈를 조절해야한다..!

이게 무슨 소리인지 모르겠으니,, 구현한 모습을 보겠다!

 

이런 모습이다..! 이것을 활용해서 만들 수 있는 것이 다양하니 알아두자..!

//
//  MoodViewController+Carousel.swift
//  Mohaeng
//
//  Created by 김승찬 on 2021/09/16.
//

import UIKit

class CarouselLayout: UICollectionViewFlowLayout {
    
    public var sideItemScale: CGFloat = 0.5
    public var sideItemAlpha: CGFloat = 0.5
    public var spacing: CGFloat = 10
    
    public var isPagingEnabled: Bool = false
    private var isSetUp: Bool = false
    
    override public func prepare() {
        super.prepare()
        if isSetUp == false {
            setupLayout()
            isSetUp = true
        }
    }
    
    //  prepare()가 처음으로 호출될 때 컬렉션 뷰에 대한 초기 설정을 하기 위해, setupLayout()이라는 함수 생성
    //  섹션 인셋, 미니멈라인 스페이싱 등 설정
    //  prepare는 사용자가 스크롤 시 매번 호출되기 때문에, isSetup이라는 프로퍼티를 만들어 초기에 딱 한 번만 호출되도록 함.
    
    private func setupLayout() {
        guard let collectionView = self.collectionView else { return }
        
        let collectionViewSize = collectionView.bounds.size
        
        let xInset = (collectionViewSize.width - self.itemSize.width) / 2
        let yInset = (collectionViewSize.height - self.itemSize.height) / 2
        
        self.sectionInset = UIEdgeInsets(top: yInset, left: xInset, bottom: yInset, right: xInset)
        
        let itemWidth = self.itemSize.width
        
        let scaledItemOffset =  (itemWidth - itemWidth * self.sideItemScale) / 2
        self.minimumLineSpacing = spacing - scaledItemOffset
        
        self.scrollDirection = .horizontal
    }
    
    //  shouldInvalidateLayout(forBoundsChange: )의 경우 위에서도 설명했듯이 true로 반환 함으로써 사용자가 스크롤 시 prepare()를 통해 레이아웃 업데이트가 가능하게 끔 합니다.
    
    public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
    
    //  layoutAttributesForElements(in: )는 모든 셀과 뷰에 대한 레이아웃 속성을  UICollectionViewLayoutAttributes 배열로 반환
    //  속성을 변환해서 반환할 거기 때문에 고차 함수 map을 사용.
    //  map에서 전달 인자로 받는 함수에 우리가 각 아이템들을 어떻게 변환시킬 것인지에 대한 내용
    
    public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let superAttributes = super.layoutAttributesForElements(in: rect),
              let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes]
        else { return nil }
        
        return attributes.map({ self.transformLayoutAttributes(attributes: $0) })
    }
    
    //이제 각 아이템(셀)들의 레이아웃 속성 변화를 담당할 transformLayoutAttributes 함수를 만듦.
    
    private func transformLayoutAttributes(attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        
        guard let collectionView = self.collectionView else { return attributes }
        
        let collectionCenter = collectionView.frame.size.width / 2
        let contentOffset = collectionView.contentOffset.x
        let center = attributes.center.x - contentOffset
        
        //비율을 구하기 위해서 maxDistance와 distnace를 사용
        // maxDistance는 여기서 아이템 중앙과 아이템 중앙 사이의 거리를 의미하는 고정 값 distance의 경우 maxDistance와 collectionCenter - center의 절대 값 중 더 작은 값을 의미 그래서 distance는 0~maxDistance 값을 갖게 됨.
        
        let maxDistance = self.itemSize.width + self.minimumLineSpacing
        let distance = min(abs(collectionCenter - center), maxDistance)
        
        //  (maxDistance - distance)/maxDistance, distance가 0이면 비율은 1, distance가 maxDistance이면 비율은 0, maxDistance는 고정 값이기 때문에 가변 값인 distance값의 변화에 따라 비율은 0~1
        
        let ratio = (maxDistance - distance)/maxDistance
        
        //    이제 위의 값들을 기반으로 하여 거리에 따른 비율을 계산하고 그 비율을 갖고서 alpha와 scale 값을 조정하는 공식 만듦.
        
        let alpha = ratio * (1 - self.sideItemAlpha) + self.sideItemAlpha
        let scale = ratio * (1 - self.sideItemScale) + self.sideItemScale
        attributes.alpha = alpha
        
        let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
        let dist = attributes.frame.midX - visibleRect.midX
        var transform = CATransform3DScale(CATransform3DIdentity, scale, scale, 1)
        transform = CATransform3DTranslate(transform, 0, 0, -abs(dist/1000))
        attributes.transform3D = transform
        
        return attributes
    }
    
    //  페이징 기능을 하게 해주는 메서드
    //  스크롤이 중지되는 지점을 변경 가능
    
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
        
        guard let collectionView = self.collectionView else {
            let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
            return latestOffset
        }
        
        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)
        guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }
        
        var offsetAdjustment = CGFloat.greatestFiniteMagnitude
        let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2
        
        for layoutAttributes in rectAttributes {
            let itemHorizontalCenter = layoutAttributes.center.x
            if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {
                offsetAdjustment = itemHorizontalCenter - horizontalCenter
            }
        }
        
        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)
    }
}

위의 코드는 UICollectionViewFlowLayout 객체를 Custom해서 새로운 클래스로 상속받는다.

컬렉션뷰를 사용할 때에는 UICollectionViewDelegateFlowLayout 를 사용하는데 여기서는 특이하게 UICollectionViewFlowLayout 를 사용한다. (Delegate보다 간단한 느낌을 받는다.)

 

UICollectionViewFlowLayout 객체를 활용하는 경우는 엄청 복잡한 레이아웃을 커스텀해야하는 경우나 간단하게 초기 값을 설정하기 위해 사용한다.

UICollectionViewFlowLayoutDelegate 같은 경우는 마찬가지로 데이터를 이용해서 분기처리로 레이아웃을 설정해야하는 경우에 사용한다.

 

코드를 간략하게 설명하자면 우선 동영상에서 나오듯이 가운데 셀 양 옆에는 알파값이 주어지고 크기가 작아져야 한다.

그래서 CarouselLayout 이라는 클래스를 만들어주고 내가 사용할 뷰컨에서 호출하는 방식으로 사용하였다.

 

 override public func prepare() {
        super.prepare()
        if isSetUp == false {
            setupLayout()
            isSetUp = true
        }
    }
    
    //  prepare()가 처음으로 호출될 때 컬렉션 뷰에 대한 초기 설정을 하기 위해, setupLayout()이라는 함수 생성
    //  섹션 인셋, 미니멈라인 스페이싱 등 설정
    //  prepare는 사용자가 스크롤 시 매번 호출되기 때문에, isSetup이라는 프로퍼티를 만들어 초기에 딱 한 번만 호출되도록 함.

코드리뷰 받고 이 부분에 대해 고민을 하고 있다.

prepare() 가 처음으로 호출될 떄 컬렉션 뷰에 대한 초기 설정을 하기 위해 만들고

사용자가 스크롤 시 매번 호출이 되고 isSetUp 이라는 프로퍼티를 만들어 초기에 딱 한 번만 호출되도록 하였다.

하지만 초기에 딱 한 번만 호출 되는거라면 override init 을 이용해도 좋을 것 같다고 해서 수정을 해보았는데 되지 않아서,,

이 부분은 리팩토링 할 때 조금 더 공부하고 수정을 해봐야 할 것 같다.

 

회전목마라는 용어도 처음 들어보고 낯설어서 ,, 요 블로그 참고해서 구현했는데 감사합니다 ..!

https://lsh424.tistory.com/54

 

[iOS] CollectionView 3D 전환, carousel effect 주기 (1)

오늘은 아래와 같은 컬렉션 뷰를 구현하는 방법을 알려드리려고 합니다. 참고! 제 글을 읽기전에 기초적인 UICollectionView에 대한 이해를 필요로 합니다! [iOS] UICollectionView 기초 정리(1), [iOS] UICollec.

lsh424.tistory.com

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함