[SwiftUI] Carousel View 만들기
- iOS
- 2022. 9. 8.
AppStore의 App 탭의 각 섹션을 나타내는 뷰를 유사하게 만들어보고자 합니다.
요구 사항으로는 Scroll Snap이 가능하며, 스크롤시 양 옆의 콘텐츠 일부가 노출되어야 합니다.
일반적으로 해당 기능을 수행하는 뷰를 Carousel View라고 명칭하는 것 같습니다.

Carousel View를 분석한 구조는 다음과 같습니다.

- horizontal spacing: 콘텐츠 전체 영역이 시작되고 끝나는 지점을 정의합니다.
- spacing: 각 콘텐츠 영역 사이의 마진 값 입니다.
- visible rect: 부모 뷰에서 바라보는 사각형 영역 입니다.
- scrollOffsetX: x축의 오프셋 값 입니다.
Carousel View는 visible rect를 오프셋을 이동시키며 콘텐츠 뷰를 순서대로 볼 수 있는 기능을 수행합니다.
구현 과정
1. ContentView를 정의하기 위해 `@ViewBuilder` 클로저를 주입 받는 뷰를 만듭니다.
@ViewBuilder 클로저를 통해 하나의 뷰를 주입받을 수 있습니다.
2. ContentView(`@ViewBuilder` 클로저)를 여러개 받을 수 있는 구조로 만듭니다.
페이지 갯수 정보를 의미하는 `pageCount` 를 입력 받아 ForEach문을 사용해 `@ViewBuilder` 클로저를 수행하도록 합니다.
페이지 뷰(ContentView) 들은 HStack으로 감싸도록 하고 spacing 값도 입력 받습니다.
이제 `ForEach(0..<pageCount, id:\.self)` 문을 통해 페이지 인덱스에 따라 UI를 구성할 수 있습니다.
다음으로 드래그를 하여 오프셋을 이동시킬 수 있는 기능을 추가하도록 합니다.
3. DragGesture를 이용해 오프셋 값을 이동시킵니다.
(콘텐츠가 중앙 정렬이 되도록 작성하였습니다.)
// | |
// Carousel.swift | |
// Carousel | |
// | |
// Created by junyng on 2022/08/22. | |
// | |
import SwiftUI | |
struct Carousel<Content: View>: View { | |
typealias PageIndex = Int | |
let pageCount: Int | |
let visibleEdgeSpace: CGFloat | |
let spacing: CGFloat | |
let content: (PageIndex) -> Content | |
@GestureState var dragOffset: CGFloat = 0 | |
@State var currentIndex: Int = 0 | |
init( | |
pageCount: Int, | |
visibleEdgeSpace: CGFloat, | |
spacing: CGFloat, | |
@ViewBuilder content: @escaping (PageIndex) -> Content | |
) { | |
self.pageCount = pageCount | |
self.visibleEdgeSpace = visibleEdgeSpace | |
self.spacing = spacing | |
self.content = content | |
} | |
var body: some View { | |
GeometryReader { proxy in | |
let baseOffset: CGFloat = spacing + visibleEdgeSpace | |
let pageWidth: CGFloat = proxy.size.width - (visibleEdgeSpace + spacing) * 2 | |
let offsetX: CGFloat = baseOffset + CGFloat(currentIndex) * -pageWidth + CGFloat(currentIndex) * -spacing + dragOffset | |
HStack(spacing: spacing) { | |
ForEach(0..<pageCount, id: \.self) { pageIndex in | |
self.content(pageIndex) | |
.frame( | |
width: pageWidth, | |
height: proxy.size.height | |
) | |
} | |
.contentShape(Rectangle()) | |
} | |
.offset(x: offsetX) | |
.gesture( | |
DragGesture() | |
.updating($dragOffset) { value, out, _ in | |
out = value.translation.width | |
} | |
.onEnded { value in | |
let offsetX = value.translation.width | |
let progress = -offsetX / pageWidth | |
let increment = Int(progress.rounded()) | |
currentIndex = max(min(currentIndex + increment, pageCount - 1), 0) | |
} | |
) | |
} | |
} | |
} |
실행화면

참고 자료
'iOS' 카테고리의 다른 글
iOS File System (0) | 2021.04.04 |
---|---|
NSCache와 Purgeable Memory (0) | 2021.04.02 |
OptionSet을 준수하는 커스텀 타입 작성하기 (0) | 2021.04.01 |
Render loop 정리 (작성중) (0) | 2021.03.28 |
CustomStringConvertible (0) | 2021.03.27 |