iOS

[iOS-SwiftUI] ScrollView Paging 처리

i-moo 2024. 3. 6. 20:52
반응형

 

현재 개발하고 있는 프로젝트에 ScrollView Paging 처리해야하는 UI를 구현해야했다.

ScrollView 하단에 PageControl도 가능해야했음!!

참고로 iOS min버전은 iOS 15.0!

 

 

SwiftUI의 ScrollView에서 제공해주는 Paging 기능이 없었고,

별도 Gesture를 등록해서 개발한 내용들을 보긴 했으나,

프로젝트에서 SwiftUIIntrospect 라이브러리를 사용 중이라 간단하게 페이징처리 해주었다.

 

SwiftUIIntrospect 라이브러리에서 introspect ViewModifier를 사용하여 UIKit의 UIScrollView를 사용 가능하다.

https://github.com/siteline/swiftui-introspect

 

GitHub - siteline/swiftui-introspect: Introspect underlying UIKit/AppKit components from SwiftUI

Introspect underlying UIKit/AppKit components from SwiftUI - siteline/swiftui-introspect

github.com

 

ScrollView PagingEnabled

 

1. 파라미터로 전달받은 UIScrollView의 isPagingEnabled 속성을 설정해주면된다.

UIScrollView에서 페이징 처리를 할 때 사용하던 속성 그대로 사용해주면 된다.

scrollView.isPagingEnabled = isPagingEnabled

ScrollView(.horizontal, showsIndicators: false) {
    LazyHStack(alignment: .top, spacing: 0) {
        ForEach(page, id: \.self) { value in
            // Paging Cell
        }
    }
}
.introspect(.scrollView, on: .iOS(.v15, .v16, .v17)) { scrollView in
    scrollView.isScrollEnabled = isPagingEnabled
    scrollView.isPagingEnabled = isPagingEnabled
}

 

2. Cell 사이즈 조정

페이징 처리가 정상적으로 작동하기 위해서는

GeometryReader를 이용하여 Paging Cell 높이나 넓이를 스크롤뷰 페이지 영역에 맞춰서 frame 설정을 해줘야한다.

GeometryReader { proxy in
    ScrollView(.horizontal, showsIndicators: false) {
        LazyHStack(alignment: .top, spacing: 0) {
            ForEach(page, id: \.self) { value in
                // Paging Cell
            }
            .frame(width: proxy.size.width,
                   height: proxy.size.height)
        }
    }
    .introspect(.scrollView, on: .iOS(.v15, .v16, .v17)) { scrollView in
        scrollView.isScrollEnabled = isPagingEnabled
        scrollView.isPagingEnabled = isPagingEnabled
    }
}

 

이렇게 하면 사실 페이징뷰 자체는 끝!

 


 

ScrollView PageControl

3. PagingScrollViewModifier를 구현하여 changedPaging를 통해 연결해주었다!

 

PagingScrollViewModifier는 설명하자면 긴데... 밑에 전체 코드 github 주소 남겨놓을게요.

내용을 짧게 설명하자면 ScrollView ContentView 영역의 background frame origin가 변경될 때,
onPreferenceChange를 통해서 origin CGPoint 변경된 값을 받아 현재 페이징 연산 처리할 수 있다.

 

일단 봐야할 부분이 changedPaging!

View Extension으로 호출 가능하도록 하고 내부적으로 PagingScrollViewModifier modifier 해주고 있다.

현재 pageSize를 파라미터로 전달받고 있는데 페이징 사이즈를 전달해주면 된다.
2번에서 스크롤뷰 페이지 영역에 맞춰서 frame 설정한 사이즈!

offset 값을 가지고 현재 페이지를 계산할 때 필요하다.

원래 PagingScrollViewModifier 내부적으로 Geometry 사용하여 해보려고 했으나,
 적용하니 스크롤, 페이징이 뭐가 잘 안되더라..?

일단 추가 확인할 시간이 없어서 그냥 파라미터로 전달받음...!

extension View {
    /// axes == .horizontal인 경우, pageSize width
    /// axes == .vertical인 경우, pageSize height
    func changedPaging(name: String, 
                       axes: Axis.Set = .horizontal,
                       pageSize: CGFloat = 0,
                       page: Binding<Int>,
                       maxPage: Int) -> some View {
        modifier(PagingScrollViewModifier(name: name, axes: axes, pageSize: pageSize, page: page, maxPage: maxPage))
    }
}

 

그리고 PagingScrollViewModifier의 onPreferenceChange에서 offset 값을 가지고 현재 페이지 계산하는 부분

content
    // .background 생략
    .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
        var newPage = 0
        
        if axes == .horizontal {
            if pageSize != 0 {
                let x = -value.x
                newPage = Int(x / pageSize)
            }
        } else if axes == .vertical {
            if pageSize != 0 {
                let y = -value.y
                newPage = Int(y / pageSize)
            }
        }
        
        // newPage 음수일 경우,
        newPage = max(0, newPage)
        
        // newPage가 maxPage 높을 경우,
        if newPage > maxPage {
            newPage = maxPage
        }
        
        if page != newPage {
            // binding page 값 세팅
            page = newPage
        }
    }

 

PagingScrollViewModifier와 changedPaging을 다 구현했다면, ScrollView와 연결

ScrollView에는 coordinateSpace 추가
ScrollView ContentView에는 changedPaging 추가

위에서 살짝 설명한 background frame origin을 추출할 때 ScrollView bounds를 가져와야하기때문에 ScrollView에 coordinateSpace를 통해서 이름(key)등록이 필요하다.

changedPaging에도 동일한 이름(key)를 파라미터로 넘겨주어야한다.

ScrollView(.horizontal, showsIndicators: false) {
    LazyHStack(alignment: .top, spacing: 0) {
        ForEach(page, id: \.self) { value in
			// Paging Cell
        }
        .frame(width: proxy.size.width,
               height: proxy.size.height)
    }
    .changedPaging(name: Self.kPagingScrollName,
                   pageSize: proxy.size.width,
                   page: $currentPage,
                   maxPage: page.count-1)
}
// introspect 생략
.coordinateSpace(name: Self.kPagingScrollName)

 

4. PageControl UI 구현

너무 막 적은것 같지만,,,, 여기까지 잘 따라왔다면 ScrollView는 paging이 될거고,
currentPage에는 스크롤을 할 때마다 변경된 페이지 값이 잘 넘어오고 있는 상태입니다.

 

SwiftUI Circle을 이용해서 UI 구현

isPagingEnabled가 true인 경우에만 하단 PageControl 표시.

VStack {
    // ScrollView 생략

    if isPagingEnabled {
        HStack(spacing: 9) {
            ForEach(0..<page.count, id: \.self) { page in
                Circle()
                    .frame(width: 10, height: 10)
                    .foregroundColor(currentPage == page ? .cyan : .gray)
            }
        }
    }
}

 

샘플 코드

https://github.com/gr-kim-94/PagingScrollView

 

GitHub - gr-kim-94/PagingScrollView

Contribute to gr-kim-94/PagingScrollView development by creating an account on GitHub.

github.com

 

샘플 프로젝트 화면

 

 

 

반응형