티스토리 뷰
더 나아진 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
'SwiftUI' 카테고리의 다른 글
[Swift UI] SwiftUI 개발 공식문서 튜토리얼 실습[3] (0) | 2023.03.30 |
---|---|
[Swift UI] SwiftUI 개발 공식문서 튜토리얼 실습[2] (0) | 2023.03.28 |
[Swift UI] SwiftUI 개발 공식문서 튜토리얼 실습[1] (1) | 2023.03.27 |