일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- Performance
- Network
- RxSwift
- SwiftUI
- Animation
- combine
- WWDC
- GCD
- withAnimation
- state
- iphone
- ios
- 네트워크
- 달력
- UIKit
- dataflow
- firebase
- auth
- CS
- avsession
- view
- Concurrency
- 최적화
- arkit
- swift
- stateobject
- toolbarvisibility
- gesture
- authentication
- 접근성제어
Archives
- Today
- Total
XLOG
[SwiftUI] Infinity Carousel 구현 본문
오랜만에 포스팅이다.
두 가지 버전으로 InfinityCarousel 을 구현해볼 예정이다.
그 전에 알아야 할 것은 두가지 버전에 기본 원리는 똑같다.
만약 4가지의 ImageView를 돌리려고 가정을 해보자.
let colors: [Color] = [.red, .green, .blue, .yellow]
@State var currentIndex: Int = 0
GeometryReader { reader in
let width = reader.size.width
TabView(selection: $currentIndex) {
ForEach(colors, id: \.self) { color in
Rectangle()
.fill(color)
.frame(width: width)
}
}
}
이런식으로 보통 작성을 한다. 하지만 Infinity 하도록 스크롤이 되도록 하고 싶다면 한 가지 트릭이 필요하다.
실제 우리가 사용해야할 Data Array 가 아닌 View를 위한 새로운 배열을 만들어야 한다.
기존 빨강 -> 그린 -> 블루 -> 옐로우 였다면, 옐로우 -> 빨강 -> 그린 -> 블루 -> 옐로우 -> 빨강 이런식의 배열을 새로 생성해야한다.
즉 첫번째 항목은 젤 끝에 하나 더 추가, 제일 마지막 요소는 0번 Index에 추가를 해줘야 한다.
이렇게 추가 를 하고 양끝에 View가 가운데 위치를 하게되는 순간 바꿔치기를 하는 것이다. 5번 인덱스의 빨강이 가운데 위치하면, 1번 빨강으로, 0번 옐로우가 나타나면 4번 옐로우로 위치를 시키는 방법이다. 위치는 PreferenceKey를 이용하여 가져와서 진행상황에 맞게 조절을 해주면 된다.
struct ColorPage: Identifiable, Equatable {
var id: UUID = UUID()
let color: Color
}
struct OffsetKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
extension View {
// PreferenceKey를 이용하여, View의 위치 값을 확인
@ViewBuilder
func offsetX(perform: @escaping (CGRect) -> ()) -> some View {
self
.frame(maxWidth: .infinity)
.overlay {
GeometryReader {
let rect = $0.frame(in: .global)
Color.clear
.preference(key: OffsetKey.self, value: rect)
.onPreferenceChange(OffsetKey.self, perform: perform)
}
}
}
}
TabView(selection: $currentPage, content: {
ForEach(fakedPages) { page in
Rectangle()
.fill(page.color)
.frame(width: 300, height: size.height / 2)
// tag 값으로 현재 위치한 page의 아이디값으로 currentPage에 할당
.tag(page.id.uuidString)
.offsetX() { rect in
if currentPage == page.id.uuidString {
let minX = rect.minX
let pageOffset = minX - (size.width * CGFloat(fakeIndex(page)))
let pageProgress = pageOffset / size.width
// 현재 Tag 에 해당하는 View가 위치할 때, 혹은 움직임이 시작이 될 때 양끝의 View는 그 안쪽의 View 와 바꿔주어 Infinity Carousel 구현
if -pageProgress < 1.0 {
if fakedPages.indices.contains(fakedPages.count - 1) {
currentPage = fakedPages[fakedPages.count - 1].id.uuidString
}
}
if -pageProgress > CGFloat(fakedPages.count - 1) {
if fakedPages.indices.contains(1) {
currentPage = fakedPages[1].id.uuidString
}
}
}
}
}
})
.tabViewStyle(.page(indexDisplayMode: .never))
두번째 HStack과 Gesture를 이용한 방법이다. 이 방법은 이전 포스팅에서 WeeklyCalendar 를 구현하려고 고민했던 방법에 위에 방법을 결합하여 작성하였다.
@State var colors: [Color] = [.red, .blue, .yellow, .purple, .green]
@State var offset: CGFloat = .zero
@State var fakedColors: [Color] = []
@GestureState var gestureOffset: CGFloat = .zero
@State var currentIndex: CGFloat = .zero
@State var carouselWidth: CGFloat = .zero
var body: some View {
ZStack {
GeometryReader { reader in
let width = reader.size.width
ZStack {
VStack {
Spacer()
HStack(spacing: 0) {
ForEach(fakedColors, id: \.self) { color in
GeometryReader{ reader in
var scale = getScale(reader)
var degree = (1 - getScale(reader)) * 180
RoundedRectangle(cornerRadius: 8)
.fill(color)
.rotation3DEffect(.degrees(-degree), axis: (0, 1, 0))
.scaleEffect(scale)
.padding()
}
.frame(width: width, height: width)
}
}
.offset(x: offset + gestureOffset)
.gesture(
DragGesture()
.updating($gestureOffset) { dragValue, gestureState, _ in
gestureState = dragValue.translation.width
}
.onEnded( { value in
print(value.translation.width)
if value.translation.width < -100 {
offset += value.translation.width
if Int(currentIndex) < fakedColors.count - 1 {
currentIndex += 1
}
withAnimation(.linear(duration: 0.2)) {
offset = width * currentIndex * -1
}
if Int(currentIndex) == fakedColors.count - 1 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
currentIndex = 1
offset = width * currentIndex * -1
}
}
} else if value.translation.width > 100 {
offset += value.translation.width
if Int(currentIndex) > 0 {
currentIndex -= 1
}
withAnimation(.linear(duration: 0.2)) {
offset = width * currentIndex * -1
}
if Int(currentIndex) == 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
currentIndex = CGFloat(fakedColors.count - 2)
offset = width * currentIndex * -1
}
}
}
})
)
Spacer()
}
}
.frame(alignment: .center)
.onAppear {
carouselWidth = width
fakedColors = colors
if let firstColor = colors.first, let lastColor = colors.last {
currentIndex = 1
fakedColors.insert(lastColor, at: 0)
fakedColors.append(firstColor)
offset = width * currentIndex * -1
}
}
}
Github 주소 : https://github.com/profit0124/InfinityCarousel/tree/main/InfinityCarousel
'Swift > SwiftUI' 카테고리의 다른 글
[SwiftUI] View 의 Size 구하기 (0) | 2024.05.19 |
---|---|
[SwiftUI] Button 의 highlighted 감지하기 (0) | 2023.12.01 |
[SwiftUI] Camera Shutter Button Animation (0) | 2023.09.18 |
[SwiftUI] Animation 적용 기본 이론편 (0) | 2023.08.07 |
[SwiftUI] QR Code Scanner 만들기 (SwifUI 에 ViewController 사용하기) (1) | 2023.03.08 |