본문 바로가기
프로그래밍

[SwiftUI] 뷰 페이저 만들기 (Paged scroll view)

by hansoo.labs 한수댁 2020. 9. 25.

PagerView 미리보기

 

PagerView

// 페이지 단위 뷰를 보여줌
struct PagerView<Content: View>: View {
    let pageCount: Int
    let content: Content
    @Binding var currentIndex: Int
    @State private var dragOffset: CGFloat = 0
    @State private var pageOffset: CGFloat = 0
    // 드래그 없이 처음 레이아웃의 오프셋을 계산하기 위한 조건
    @State private var isFirstLayout = true
    
    init(pageCount: Int, currentIndex: Binding<Int>, @ViewBuilder content: () -> Content) {
        self.pageCount = pageCount
       self._currentIndex = currentIndex
        self.content = content()
    }
    var body: some View {
    	// GeometryReader로 부모에서 정해주는 크기를 페이지 사이즈에 반영
        GeometryReader { geometry in
            ZStack(alignment: .bottom) {
                HStack(spacing: 0) {
                    self.content.frame(width: geometry.size.width)
                }
                .frame(width: geometry.size.width, alignment: .leading)
                .offset(x: isFirstLayout ? -CGFloat(self.currentIndex) * geometry.size.width : pageOffset)
                .animation(Animation.default)
                .gesture(
                    DragGesture(minimumDistance: 20) // 최소 단위를 지정하면, 다른 세로 스크롤 제스쳐와 겹침을 피할 수 있음
                        .onChanged({ (value) in
                            if pageCount < 2 { return }
                            let delta = value.translation.width
                            if (delta > 0 && currentIndex == 0) || (delta < 0 && currentIndex == pageCount - 1) {
                                dragOffset = delta / 3.0
                            } else {
                                dragOffset = delta
                            }
                            pageOffset = -CGFloat(self.currentIndex) * geometry.size.width + dragOffset
                            isFirstLayout = false
                        })
                        .onEnded({ (value) in
                            if pageCount < 2 { return }
                            var newPage = self.currentIndex
                            let threshold = geometry.size.width * 0.5
                            if dragOffset > threshold  && currentIndex != 0 {
                                newPage -= 1
                            } else if dragOffset < -threshold && currentIndex != pageCount - 1 {
                                newPage += 1
                            }
                            currentIndex = newPage
                            dragOffset = 0.0
                            pageOffset = -CGFloat(newPage) * geometry.size.width
                            debugPrint("onEnded")
                        })
                )
                
                // indicator
                if pageCount > 1 {
                    HStack(spacing: 10) {
                        ForEach(0..<self.pageCount, id: \.self) { index in
                            Circle()
                                .frame(width: index == self.currentIndex ? 10 : 8,
                                       height: index == self.currentIndex ? 10 : 8)
                                .foregroundColor(index == self.currentIndex ? Color.white : Color(.white20))
                                .overlay(Circle().stroke(Color.gray, lineWidth: 1))
                                .padding(.bottom, 20)
                                .animation(.spring())
                        }
                    }
                }
            }.frame(width: geometry.size.width, height: geometry.size.height)
        }
    }
}

struct PagerView_Previews: PreviewProvider {
    static var previews: some View {
        PagerView(pageCount: 3, currentIndex: .constant(0)) {
            Color.blue
            Color.red
            Color.green
        }
    }
}

 

 사용법

// 위에 미리보기 화면에 쓰인 코드
ZStack(alignment: .topLeading) {
    PagerView(pageCount: imageUrls.count, currentIndex: $currentPage) {
        // 페이지단위 컨텐츠들
        ForEach(imageUrls, id: \.self) { url in
            // 외부라이브러리: https://github.com/crelies/RemoteImage
            RemoteImage(
                type: .url(url),
                errorView: { error in
                    Text(error.localizedDescription)
                }, imageView: { image in
                    image.resizable()
                }, loadingView: {
                    Text("loading__")
                        .font(.headline)
                        .foregroundColor(Color(.secondaryLabel))
                })
        }
    }
    // 라벨뷰
    self.nameLabelView
}
.frame(maxWidth: .infinity)
.aspectRatio(1.0, contentMode: .fit)
.padding(.bottom, 20)

댓글0