-
Notifications
You must be signed in to change notification settings - Fork 2
[승용] Beyond scroll views
Eric Kwon / 권승용 edited this page Sep 5, 2024
·
1 revision
-
컨텐트를 스크롤 가능하게 해주는 building block
-
스크롤 가능한 축을 결정하는 axes 파라미터 가짐
-
content를 가짐.
-
content가 스크롤 뷰의 크기를 넘어가면 content는 clipped 될 것이고, 스크롤 해야 해당 컨텐트를 찾을 수 있을 것
-
ScrollView는 content가 safe area 내에 배치되도록 보장하며, safe area를 컨텐츠 외부 margin으로 변환해 content가 safe area 내에 배치되도록 한다.
-
ScrollView는 기본적으로 내부 content를 즉시(eagerly) 계산한다.
- Lazy Stack을 사용해 아이템이 보여질 때 계산하도록 변경 가능
-
ScrollView가 content 내에서 스크롤한 정확한 위치를 content offset이라고 한다.
-
ScrollViewReader를 사용해서 content offset을 수정할 수 있었음
-
2023년부터는 SwfitUI에서 ScrollView의 content offset으로 할 수 있는 더 많은 요소들을 제공
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
HeroView(palette: palette)
}
}
}
- 수평 스크롤 뷰를 구현했을 때 content의 왼쪽 앞에 살짝 공간을 주고 싶음
- padding을 사용하면 content의 양쪽 모두 잘림
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
HeroView(palette: palette)
}
}
}
.padding(.horizontal, hMargin)
- ScrollView 자체에 패딩을 주는 대신 content 마진을 확장시키면 된다.
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
HeroView(palette: palette)
}
}
}
.safeAreaPadding(.horizontal, hMargin)
- 이렇게 하면 content에 패딩을 주는 대신 safe area에 패딩을 준다.
- 따라서 다음 content가 보임
- safe area는 주로 앱이 실행되는 디바이스에서 제공되지만, .safeAreaPadding이나 .safeAreaInset 모디파이어 등의 API로부터 제공될 때도 있다.
- ScrollView는 safe area를 content에 적용하는 마진(여백)으로 변환한다.
- content에는 사용자가 만든 content 뿐만 아니라 ScrollView가 책임지는 추가적인 content(스크롤 인디케이터 등)도 포함된다.
- 이는 safe area를 수정해 각기 다른 content마다 각기 다른 inset을 설정할 수 없다는 것을 의미한다.
- 만약 다른 inset을 적용하고 싶다면 새로운 .contentMargins API를 사용 가능
ScrollView {
// content
}
.contentMargins(
.vertical, 50.0,
for: .scrollContent // 또는 .scrollIndicators
)
!Screenshot 2024-09-05 at 7.02.29 PM.png
- safe area와 content margin이 달리 적용되는 모습을 확인 가능
- 스크롤 끝날 때 어떻게 끝나는지 정하기
- 보통은 스크롤 속도와 표준 감속 비율을 적용해 스크롤이 끝나는 content offset을 계산
- 그러나 특정 지점에 멈추게 하고 싶을 수 있음
- 그럴 때 scrollTargetBehavior 사용
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
HeroView(palette: palette)
}
}
}
.contentMargins(.horizontal, hMargin)
.scrollTargetBehavior(.paging)
- .paging은 ScrollView의 containing size에 따라 자동으로 스크롤이 끝날 곳을 찾아줌
- 그러나 얘는 크기 기반이라서, 크기가 커지면 의도한 바와 다르게 동작할 수도 있음 (아이패드 등)
- 그럴 땐 viewAligned 를 사용하고, scrollTarget을 모디파이어를 통해 설정해 개별적인 뷰에 멈추도록 설정 가능
- Lazy Stack에서는 scrollTarget으로 개별적인 뷰를 지정하면 안 됨. scrollTargetLayout 모디파이어를 사용해 레이아웃 자체에 스크롤 타겟 적용. 왜냐하면 전체 개별적인 뷰가 로드되지 않기 때문!
- paging과 viewAligned는 ScrollTargetBehavior 프로토콜에 기반해 만들어진 built-in 동작들임
- 필요하면 해당 프로토콜을 준수해 커스텀 동작을 만들 수 있음
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
}
}
}```
- updateTarget 이라는 필수 요구사항만 구현하면 됨
## conditionalRelativeFrame
- 이전에는 상위 컨테이너의 크기에 따라 내부 내용을 바꾸려면 GeometryReader를 사용해야 했음
- 그러나 지금은 conditionalRelativeFrame을 사용해 쉽게 작업할 수 있게 되었다.
```swift
#if os(iOS)
@Environment(\.horizontalSizeClass) private var sizeClass
#endif
HeroColorStack(palette: palette)
.frame(height: 250.0)
.containerRelativeFrame(
.horizontal,
count: sizeClass == .regular ? 2 : 1,
spacing: 10.0)
- 일반적인 옵션, 조건에 따라 다른 크기를 부여하는 옵션 등 다양하게 커스텀 가능
- scrollIndicators(.hidden) 모디파이어는 macOS 환경에서 마우스와 함께 동작하게 되면 스크롤 바가 사라지지 않음
- why? 마우스는 스크롤 바가 없으면 스크롤 사용이 어려움
- scrollIndicators(.never)를 선택하면 모든 상황에서 스크롤 인디케이터가 사라짐
@State private var mainID: Palette.ID? = nil
VStack {
GallerySectionHeader(mainID: $mainID)
ScrollView(.horizontal) { ... }
.scrollPosition(id: $mainID)
}
// in GallerySectionHeader
GalleryPaddle(edge: .leading) {
mainID = previousID()
}
- scrollPosition 설정할 수 있는 새로운 모디파이어 생겼음
- 옛날엔 ScrollViewReader 썼지만 이제 scrollPosition 사용하면 됨
- Identifier를 감싸는 @State에 @Binding을 연결하는 모디파이어이다.
- 따라서 binding 값이 변하면 해당 ID를 가지는 뷰의 위치로 스크롤될 것
- scrollTargetLayout 모디파이어를 사용해 어떤 뷰를 대상으로 identity value를 조회할 것인지 결정한다.
- transition은 뷰가 나타나거나 사라질 때 겪는 변화를 정의한다.
- ScrollTransition은 일반적인 transition과 다름
- 보통 transition은 뷰가 나타날 때 아무런 커스텀화도 이루어지지 않음
- 그러나 ScrollTransition은 뷰가 ScrollView의 visible region에 들어올 떄와 나갈 때 적용된다.
HeroView(palette: palette)
.scrollTransition(axis: .horizontal)
{ content, phase in
content
.scaleEffect(
x: phase.isIdentity ? 1.0 : 0.80,
y: phase.isIdentity ? 1.0 : 0.80)
}
- 뷰가 visible region의 가운데에 있으면 ScrollTransition의 identity phase에 있는 것이다.
- 따라서 identity phase라면 원래 크기를, 그렇지 않고 이동 중이라면 조금 줄어든 크기를 적용하면 예쁜 스크롤뷰 만들 수 있음
- ScrollTransition은 VisualEffect 프로토콜을 사용해 만들어짐
- 얘는 부모나 자식에 변경을 가하지 않고 visual appearance를 변화시키는 프로토콜
- 직접 준수하지 않고 모디파이어를 통해 구현함
- 이 프로토콜은 레이아웃의 기능을 안전하게 사용할 수 있는 뷰 content에 대한 사용자 정의 설정을 제공함
- scaleEffect, rotationEffect, offset 등이 이에 속함
- font등 전체적인 ScrollView content 크기를 변화시킬 수 있는 모디파이어는 사용 불가
- contentMargin
- scrollTargetBehavior
- containerRelativeFrame
- scrollPosition
- scrollTransition
권승용 | 김대황 | 김인환 | 유정주 | 윤동주 | 이준복 | 이창준 | 홍승현 |
---|---|---|---|---|---|---|---|
ericKwon95 | qwerty3345 | loinsir | jeongju9216 | yoondj98 | junbok97 | SwiftyJunnos | WhiteHyun |