티스토리 뷰

SwiftUI

[SwiftUI] Beyond scrollviews

ios상우 2024. 4. 16. 23:52

더 나아진 SwiftUI의 ScrollView에 대해 알아보기

https://developer.apple.com/videos/play/wwdc2023/10159/?time=17

 

 

스크롤뷰 더 편하게 쓸 수 있다고 해서 정리해보고자 합니다!!

대강 봐도 유용한게 되게 많으니 같이 보고 참고하시면 좋을 것 같습니다!!

 

safeAreaPadding

 

다음과 같은 화면과 코드에서 콘텐츠 여백을 주려고 .padding(.horizontal, hMargin) 코드를 추가하면 오른쪽처럼 스크롤되는 부분도 짤려보일 것임

import SwiftUI

struct BeyondScrollView: View {
    let hSpacing: CGFloat = 16
    let hMargin: CGFloat = 16
    let palettes = [Color.blue, .red, .green]
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: hSpacing) {
                ForEach(palettes.indices) { palette in
                    palettes[palette]
                        .frame(width: .screenWidth-hSpacing*2, height: 300)
                        .background()
                }
            }
        }
    }
}

#Preview {
    BeyondScrollView()
}

 

이 문제를 해결하기 위해 Content에 Padding을 주기보다는 safeArea에 Padding을 주는편이 더 좋음

.safeAreaPadding()

padding modifier와 동일한 동작을 하지만 safearea에 패딩을 넣기때문에scrollView는 전체 너비가 적용되서 다음 컨텐츠가 보임

contentMargins

contentMarginAPI  safeArea로부터 개별 인셋을 주고 싶을때 사용가능하며

indicator와 content의 마진을 별개로 줄 수 있음

 ScrollView(.horizontal) {
            LazyHStack(spacing: hSpacing) {
                ForEach(palettes, id: \\.self) { palette in
                    palette
                        .frame(height: 300)
                        .frame(width: .screenWidth-hSpacing*2, height: 300)
                }
            }
        }
        .contentMargins(hMargin , for: .scrollContent) // ✅ 콘텐트 마진 설정
        .contentMargins(24, for: .scrollIndicators) // ✅ 인디케이터 마진 설정
        .scrollTargetBehavior(.viewAligned)

 

ScrollTargetLayout()

 

보통 스크롤하다가 손가락을 뗐을 때 어느 Content offset 까지 스크롤될지는 표준 감속률과 스크롤 속도를 이용해 스크롤이 끝나야할 content offset을 계산하며 scrollview나 콘텐츠의 크기는 감안하지 않음

그러나 우리는 이게 필요할 때가 있음!! 이 때는 ScrollTargetBehavior modifier를 이용하면됨ScrollTargetBehaviorProtocol을 준수함

여기서 제공되는 .paging을 사용하면 컨테이너를 계산해 하나의 컨테이너크기만큼 페이징됨

그러나 아이패드에서 한 화면에 두개의 컨텐츠가 있을때는 두개씩 넘어가기도하고

화면에 딱맞게 보여주고 싶은데 아마 아래 사진처럼 이전 콘텐츠나 다음 콘텐츠가 조금씩 튀어나오는게 보일거임

근데 이걸 쉽게 개선할 수 있는게 나옴

→ 개별적 뷰로 정렬시켜주고싶음

→ 새로운 ScrollTarget으로 지정하면 가능함!! 개별 뷰에 scrollTargetLayout() 을 붙여주면 스크롤 타깃으로 삼고 .scrollTargetBehavior(.viewAligned)을 주면 됨

아래 코드처럼!!

  ScrollView(.horizontal) {
            LazyHStack(spacing: hSpacing) {
                ForEach(palettes, id: \\.self) { palette in
                    palette
                        .frame(height: 300)
                        .frame(width: .screenWidth-hSpacing*2, height: 300)
                }
            }
            .scrollTargetLayout()
        }
        .contentMargins(hMargin , for: .scrollContent)
        .scrollTargetBehavior(.viewAligned)

그럼 이렇게 딱 맞게 나옴~!

CustomScrollTargetBehavior

다음은 커스텀 ScrollTargetBehavior 만들기!!

ScrollTargetBehaviorProtocol 을 준수하여 커스텀한 스크롤 가능

이 프로토콜을 채택하면 필수메소드인 updateTarget을 구현해야하며

스크롤을 어디서 끝낼지 계산할때, 스크롤뷰의 크기가 바뀔때 등 호출됨

아래 코드처럼 만들면 됨

struct GalleryScrollTargetBehavior: ScrollTargetBehavior {
    func updateTarget(_ target: inout ScrollTarget, context: TargetContext) {
        if target.rect.minY < (context.containerSize.height / 3.0),
           context.velocity.dy < 0.0 {
            target.rect.origin.y = 0.0
        }
    }
}

 

containerRelativeFrame

 

ScrollView 콘텐츠의 크기를 디바이스 크기에 대응하는법

원래 Geometry로 감싸서 크기 받아오던가.. 윈도우 사이즈 받아왔었는데 ㅠㅠ

이제 containerRelativeFrame Modifier로 더 쉬워짐

@Environment(\\.horizontalSizeClass) private var sizeClass

Color.black
	.containerRelativeFrame(.horizontal, // ✅ 어느 축
							count: sizeClass == .regular ? 2 : 1, // ✅ size class별로 그리드 처럼 나타낼 수 있음
                            spacing: 16) // ✅ 간격

 

aspectRatio

 

높이 프레임을 고정시키지 않아도 비율로 상대적인 높이 가능

        ScrollView(.horizontal) {
            LazyHStack(spacing: hSpacing) {
                ForEach(palettes, id: \\.self) { palette in
                    palette
                        // .frame(height: 300) // ❌ 제거
                        .aspectRatio(4.0/3.0, contentMode: .fit) // ✅ 4:3 비율로
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .contentMargins(hMargin , for: .scrollContent)
        .scrollTargetBehavior(.viewAligned)

scrollIndicators

 

modifier로 스크롤 인디케이터 숨김 가능

.scrollIndicators(.hidden) : 손가락, 트랙패드등은 숨겨짐 but 마우스로 스크롤하면 나옴 for 사용성

.scrollIndicators(.never): 마우스고 뭐고 안나옴

 

Targets and Positions

 

Scroll Target과 ScrollPosition을 통해 스크롤 뷰의 컨텐츠 오프셋을 관리하는 방법!

struct BeyondScrollView: View {
    @Environment(\\.horizontalSizeClass) private var sizeClass
    let hSpacing: CGFloat = 16
    let hMargin: CGFloat = 16
    
    let palettes = [
        Palette.init(id: UUID(), color: .blue),
        Palette.init(id: UUID(), color: .red),
        Palette.init(id: UUID(), color: .green),
        Palette.init(id: UUID(), color: .black),
    ]
    
    @State private var mainID: Palette.ID? // ✅ 모델 ID
    
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: hSpacing) {
                ForEach(palettes, id: \\.id) { palette in
                    palette.color
                        .aspectRatio(4.0/3.0, contentMode: .fit)
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .contentMargins(hMargin , for: .scrollContent)
        .scrollTargetBehavior(.viewAligned)
        .scrollPosition(id: $mainID)  // ✅ ID에 해당하는 콘텐츠로 스크롤
        
        Button {
            scrollToPreviousID()
        } label: {
            Text("이전")
        }
    }
    
        
		// ✅ 이전 ID로 스크롤
    private func scrollToPreviousID() {
        guard let id = mainID, id != palettes.first?.id,
              let index = palettes.firstIndex(where: { $0.id == id })
        else { return }
        
        withAnimation {
            mainID = palettes[index-1].id
        }
    }
}

// ✅ Hashable한 타입이어야함
struct Palette: Identifiable {
    var id: UUID
    var name: String
    var color: Color
    
    init(id: UUID, color: Color) {
        self.id = id
        self.name = color.description
        self.color = color
    }
}

 

Scroll transitions

Scroll transitions을 이용해 스크롤뷰 멋내기!!

  • 뷰가 나타나거나 사라질때 어떤 변경사항을 넣어줄 수 있음
  • scaleEffect, rotationEffect, offSet등을 컨트롤 가능함
  • fontEffect나 스크롤뷰 전체 크기를 변경하는 속성도 불가

이 속성을 사용하면 Carousel view도 쉽게 구현이 가능!! 완전 좋네요 이 외에도 여러가지 속성들이 있으니 찾아서 해보면 좋을 것 같음!!!

       ScrollView(.horizontal) {
            LazyHStack(spacing: hSpacing) {
                ForEach(palettes, id: \\.id) { palette in
                    palette.color
                        .aspectRatio(4.0/3.0, contentMode: .fit)
                        .containerRelativeFrame(.horizontal)
                }
                // ✅ 멀어지면 스케일 축소 !! 
                .scrollTransition(axis: .horizontal) { content, phase in  
                    content
                        .scaleEffect(
                            x: phase.isIdentity ? 1.0 : 0.80,
                            y: phase.isIdentity ? 1.0 : 0.80)
                }
            }
            .scrollTargetLayout()
        }
        .contentMargins(hMargin , for: .scrollContent)
        .scrollTargetBehavior(.viewAligned)
        .scrollPosition(id: $mainID)
        
        Button {
            scrollToPreviousID()
        } label: {
            Text("이전")
        }

 

 

🔗 Github Code

https://github.com/iosdevSW/iOS_Study/tree/master/SwiftUI/BeyondScrollView

«   2025/03   »
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 31
공지사항
링크
Total
Today
Yesterday