개발/Swift

[Swift] 페이지처럼 넘어가는 스크롤 레이아웃 (Carousel Effect)

덤벨로퍼 2025. 2. 26. 17:40

1. 레이아웃 잡기

 

제약조건을 다음과 같이 잡음

scrollView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        
        stackView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
            make.height.equalToSuperview()
        }
    

가로 스크롤의 경우 Scrollview의 높이값이 지정 되어있어야 함,

그렇지 않으면 가로스크롤 뿐만 아니라 세로스크롤 까지 동작 해버림

그런데 고정된 높이값을 지정해 줄수가 없고

stackview 내부 이미지의 사이즈에 따라 동적으로 계산되어 높이가 지정 되어야 하는 값임

stackView.arrangedSubviews.forEach { imageView in
    guard let imageView = imageView as? UIImageView else { return }
    
    imageView.snp.makeConstraints { make in
        if let imageSize = imageView.image?.size {
            make.height.equalTo(imageView.snp.width).multipliedBy(imageSize.height / imageSize.width).priority(.required)
            make.width.equalTo(scrollView).offset(-Metric.contentsMargin * 2)
        }
    }
}

이렇게 구현하면 가로 뿐만 아니라 세로 스크롤까지 적용 되어버림,

또한 scrollView내부에 원인 모를 top inset이 들어가있음

  • 해결

scroll 뷰 내부적으로 자동 inset이 들어가있는 부분, bounce를 제거

        
        //ScrollView 속성
        $0.contentInsetAdjustmentBehavior = .never //inset 제거
        $0.bounces = false

→ 좌우 bounce도 막히지만 세로 스크롤 해결은 가능

 

2.Carousel 효과

 

페이지 처럼 넘어가는 스크롤을 구현 하려면 속성을 통해 쉽게 가능

 $0.isPagingEnabled = false

Stackview 의 아이템의 너비가 Scrollview 너비와 같다면 문제가 없지만

ScrollView 좌우 inset이 들어가거나 Stackview 내부 spacing 이 있는경우

그 너비가 달라지므로 계산 법이 달라져서 페이지가 이상하게 넘어감

  • 해결
    • 직접 계산해서 구현
//ScrollView 속성

$0.isPagingEnabled = false 
$0.decelerationRate = .fast
$0.contentInset = .init(top: 0, left: Metric.contentsMargin, bottom: 0, right: Metric.contentsMargin)

// delegate 추가
scrollView.delegate = self

UIScrollViewDelegate - scroolViewWillEndDragging 통해서 페이징 구현 가능

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint,
                                   targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        //양 옆 마진 - stackview spacing 를 Scrollview 전체 너비에서 뺴면
        // 하나의 아이템 너비를 의미함 (imageViewWidth)
        let totalMargin = Metric.contentsMargin * 2 - Metric.stackViewSpacing
        let imageViewWidth = scrollView.frame.width - totalMargin
        // 반올림으로 인덱스 구할수있음 
        let index = round(targetContentOffset.pointee.x / imageViewWidth)
        let offset = CGPoint(x: index * imageViewWidth - Metric.contentsMargin, y: 0)
        targetContentOffset.pointee = offset
        reactor?.action.onNext(.pageScroll(pageIndex: Int(index)))
    }

index는 오차범위가 좀 있어도 어느정도 반 이상만 넘겼다면 넘기면 되니까 상관없다.

 

근데 offset은 정확해야 원하는 위치에 다음페이지 위치할수 있다.

 

offset은 결국 이미지 사이즈 * index만큼 넘겨주고

사이드 inset이 지정 되었다면 그만큼만 빼줘야 왼쪽 여백이 있는 상태만큼 넘어간다.

사이드 inset이 없다면 제외해도 된다.

 

또 ScrollView의 contentInset이 아니라 Stackview contentInset으로 지정되었다면 또 계산법이

달라지므로 주의 필요하다

 

만약 어렵다면 그냥1번쨰 2번째 3번쟤 페이지 다계산해서 공식 세우는게 빠를수도 있다.

    targetContentOffset.pointee = offset

마지막 이부분만 세팅해주면 알아서 페이지 넘어감