안녕하세요. 린다입니다.
두 개의 프로젝트에서 Carousel 뷰를 모두 사용 중인데요.
17부터는 다양한 modifier가 있는 carousel 을 사용할 수 있지만..
최소 지원버전이 모두 16이기 때문에 커스텀으로 진행했어요.
프로퍼티 설명
edgeSpacing: 두번째 카드부터 보이는 양옆에 나와있는 카드들의 크기
contentSpacing: 카드와 카드 사이의 spacing
totalSpacing: carousel뷰가 들어가는 mainView에서의 전체 padding
contentHeight: carousel뷰 height
carouselContent: carousel를 구성할 View (@escaping == 밖에서 정의해서 사용)
totalSpacing의 마지막 padding은 필요에 따라 빼고 사용하셔도 됩니다.
메인코드
public struct CarouselView<Data: Identifiable, Content: View>: View {
public let data: [Data]
public let edgeSpacing: CGFloat
public let contentSpacing: CGFloat
public let totalSpacing: CGFloat
public let contentHeight: CGFloat
public let carouselContent: (Data) -> Content
@State public var currentIndex: CGFloat = 0
@State public var currentOffset: CGFloat = 0
public init(
data: [Data],
edgeSpacing: CGFloat,
contentSpacing: CGFloat,
totalSpacing: CGFloat,
contentHeight: CGFloat,
@ViewBuilder carouselContent: @escaping (Data) -> Content
) {
self.data = data
self.edgeSpacing = edgeSpacing
self.contentSpacing = contentSpacing
self.totalSpacing = totalSpacing
self.contentHeight = contentHeight
self.carouselContent = carouselContent
}
public var body: some View {
VStack {
GeometryReader { geometry in
let baseOffset = contentSpacing + edgeSpacing - totalSpacing
let total: CGFloat = geometry.size.width + totalSpacing * 2
let contentWidth = total - (edgeSpacing * 2) - (2 * contentSpacing)
let nextOffset = contentWidth + contentSpacing
HStack(spacing: contentSpacing) {
ForEach(0..<data.count, id: \.self) { index in
carouselContent(data[index])
.frame(width: contentWidth, height: contentHeight)
.gesture(
DragGesture()
.onEnded { value in
let offsetX = value.translation.width
if offsetX < -50 { // 오른쪽으로 스와이프
currentIndex = min(currentIndex + 1, CGFloat(data.count - 1))
} else if offsetX > 50 { // 왼쪽으로 스와이프
currentIndex = max(currentIndex - 1, 0)
}
withAnimation {
currentOffset = -currentIndex * nextOffset
}
}
)
}
}
.offset(x: currentOffset + (currentIndex > 0 ? baseOffset : 0))
}
}
.padding(.horizontal, totalSpacing)
}
}
위에서 말했듯이 Content는 Carousel 뷰를 그리기 위한 @escaping으로써
클로저에 뷰를 그려주면 해당 뷰를 carousel 로써 사용할 수 있어요.
(참고하면 마지막 뷰는 다르게 그리기 위한 또 다른 @escaping을 추가해서 사용할 수도 있음)
여기서 왜 total 의 구성이 이렇게 되느냐를 설명해보자면,
let total: CGFloat = geometry.size.width + totalSpacing * 2
스유의 레이아웃 순서를 생각하면 padding은 상위에서 "여백"을 추가해주는 것인데
하위에서 해당 부분을 고려하지 않고 카드의 너비를 구하면 더 작게 나와요
그렇기 때문에 분명히 동일한 너비의 카드인데 왜 스와이프 정도에 따라 값이 다르지..?
를 볼 수 있습니다.
물론 이렇게 안 해도 offset을 사용하면 이런 처리를 안 해줘도 되는데요.
(맨 처음 currentIndex == 0, offset부터 처리해주면 이렇게 안 해도 됨..)
사실 맨 처음에 offset으로 만들었다가 미묘하게 피그마랑 다른것 같아서
캡쳐 후 비교했더니 역시 이상해서 padding, offset 두 버전으로 모두 만들면서 해결했어요.
offset 버전은 정리를 안 해서 padding으로 올리는 것일 뿐
(syss220211 > github > SwiftUIAnimation Repo 가시면 offset 확인 가능)
실제화면
와 성공이다! ➡ 아니네 .. 를 약 4번 정도 반복한 애증의 Carousel 끝...
혹시나 코드나 이상하다거나, 더 좋은 방법이 있다면 피드백 부탁드립니다!
'Swift > SwiftUI' 카테고리의 다른 글
SwiftUI에서 Typography 설정하기 (feat. Tuist) (0) | 2024.07.20 |
---|---|
SwiftUI, Layout 프로세스를 알아보자3 (feat, Offset와 Position) (0) | 2024.03.28 |
SwiftUI, Layout 프로세스를 알아보자2 (feat, offset과 position) (0) | 2024.03.28 |
SwiftUI, Layout과 Alignment을 알아보자 (2019 WWDC) (1) | 2024.03.28 |
SwiftUI, Custom TextEditor 만들기 (0) | 2024.03.20 |