XLOG

[SwiftUI] 캘린더용 InfinityScroll 로직에 대한 아이디어 본문

Swift/SwiftUI

[SwiftUI] 캘린더용 InfinityScroll 로직에 대한 아이디어

X_PROFIT 2023. 2. 27. 21:47

작년에 SwiftUI를 공부하면서 캘린더를 만들어야 하는 상황이 있었다.

단순히 버튼을 눌러 값을 변화시키는 것이라면 조금은 간단할 수 있지만, 기본 제공 달력 처럼 Scroll Animation 효과가 있어야 한다.

 

기본적인 Infinity Scroll의 경우 한 방향으로 지속적으로 작동, 값을 추가만 하면 된다.

하지만 달력의 경우 좌, 우 즉 값이 추가되거나 감소되거나 해야한다.

 

무작정 사람이 스크롤 할 만한 범위의 값을 불러와서 만들기는 싫었다.

내가 생각한 아이디어는 3주치의 배열을 계속해서 바꿔주는 것이다. Paging을 사용하게 되면 쉽게 될 줄 알았지만 값의 변화에 따른 View의 재생성되는 과정이 매끄럽지 못했다. 그 당시 생각할 수 있었던 아이디어는 HStack 의 특성을 이용하여 offset 을 DragGesture로 변화를 주고 값을 바꿔 재생성하자 밖에 떠오르지 않았다.

 

아이디어를 자세히 정리하자면

1. HStack 으로 이전 주, 현재 주, 다음 주의 뷰를 나열 후 한 주의 크기를 기기 사이즈를 고려하여 할당해준다.

2. DragGesture 로 HStack의 offset의 변화를 주어, 스크롤이 되는 것처럼 보여준다.

3. 한주의 변화가 있게 스크롤을 할 경우 HStack 의 offset 화면 크기에 맞게 변화를 준다.

4. 이때 Offset의 변화는 withAnimation 으로 애니메이션 효과를 주어 자동으로 스크롤이 되는것 처럼 연출한다.

5. DispatchQueue.main.asyncAfter 를 이용하여 애니메이션 효과가 끝나게 되면 가지고 있는 주간 데이터(일자) 의 변화를 주고, HStack의 offset을 0으로 준다.

6. State의 변화로 처음 만들어진 View는 한주가 바뀌어 있는 것처럼 새로운 View가 대체하게 된다.

 

@State var dayList:[[Int]] = [[1,2,3,4,5,6,7], [8,9,10,11,12,13,14], [15,16,17,18,19,20,21]]
@GestureState var gestureOffset: CGFloat = .zero
@State var offset: CGFloat = .zero

----------


HStack(spacing: 0) {
    ForEach(dayList.indices, id: \.self) { i in
        NumberView(numbers: $dayList[i])
    }
}
// 날짜의 경우 DragGesture가 동작할때 값과 end가 될때 offset 값을 더해서 페이지 전환 효과를 준다.
.offset(x: width*0.03 + gestureOffset + offset)

.gesture(
    DragGesture()
        .updating($gestureOffset) { dragValue, gestureState, _ in
            // drag 하는 도중 view의 offset을 실시간으로 반영해주기 위함
            gestureState = dragValue.translation.width
        }.onEnded { value in
            // 제스쳐가 종료가 되는 즉시 offset을 스크롤된 값으로 할당해주고, 스크롤 방향, 값을 기준으로 페이지 전환 시 offset을 증감, 아닐시 .zero를 할당해주되, withAnimation의 closer에 정의함으로서 애니메이션 효과를 추가한다.
            offset = value.translation.width
            if value.translation.width < -100 {
                withAnimation(.linear(duration: 0.05)) {
                    offset = width * -1
                }
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
                    nextWeek()
                }
            } else if value.translation.width > 100 {
                withAnimation(.linear(duration: 0.05)) {
                    offset = width * 1
                }
                DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
                    previousWeek()
                }
            } else {
                withAnimation(.linear(duration: 0.5)) {
                    offset = .zero
                }
            }
        }
)

Github: https://github.com/profit0124/WeeklyInfinityScrollTest