Skip to content

[승용] Beyond scroll views

Eric Kwon / 권승용 edited this page Sep 5, 2024 · 1 revision

공식문서

ScrollView

  • 컨텐트를 스크롤 가능하게 해주는 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으로 할 수 있는 더 많은 요소들을 제공

Margin

ScrollView(.horizontal) {
	LazyHStack(spacing: hSpacing) {
		ForEach(palettes) { palette in
			HeroView(palette: palette)
		}
	}
}
Screenshot 2024-09-05 at 6 50 38 PM
  • 수평 스크롤 뷰를 구현했을 때 content의 왼쪽 앞에 살짝 공간을 주고 싶음
  • padding을 사용하면 content의 양쪽 모두 잘림
ScrollView(.horizontal) {
	LazyHStack(spacing: hSpacing) {
		ForEach(palettes) { palette in
			HeroView(palette: palette)
		}
	}
}
.padding(.horizontal, hMargin)
Screenshot 2024-09-05 at 6 52 20 PM
  • ScrollView 자체에 패딩을 주는 대신 content 마진을 확장시키면 된다.
ScrollView(.horizontal) {
	LazyHStack(spacing: hSpacing) {
		ForEach(palettes) { palette in
			HeroView(palette: palette)
		}
	}
}
.safeAreaPadding(.horizontal, hMargin)
Screenshot 2024-09-05 at 7 02 29 PM
  • 이렇게 하면 content에 패딩을 주는 대신 safe area에 패딩을 준다.
  • 따라서 다음 content가 보임

Safe area?

  • safe area는 주로 앱이 실행되는 디바이스에서 제공되지만, .safeAreaPadding이나 .safeAreaInset 모디파이어 등의 API로부터 제공될 때도 있다.
  • ScrollView는 safe area를 content에 적용하는 마진(여백)으로 변환한다.
  • content에는 사용자가 만든 content 뿐만 아니라 ScrollView가 책임지는 추가적인 content(스크롤 인디케이터 등)도 포함된다.
    • 이는 safe area를 수정해 각기 다른 content마다 각기 다른 inset을 설정할 수 없다는 것을 의미한다.

Content Margin

  • 만약 다른 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이 달리 적용되는 모습을 확인 가능

scrollTargetBehavior

  • 스크롤 끝날 때 어떻게 끝나는지 정하기
  • 보통은 스크롤 속도와 표준 감속 비율을 적용해 스크롤이 끝나는 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 모디파이어를 사용해 레이아웃 자체에 스크롤 타겟 적용. 왜냐하면 전체 개별적인 뷰가 로드되지 않기 때문!

ScrollTargetBehavior Protocol

  • 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)
  • 일반적인 옵션, 조건에 따라 다른 크기를 부여하는 옵션 등 다양하게 커스텀 가능

scrollIndicator 지우기

  • scrollIndicators(.hidden) 모디파이어는 macOS 환경에서 마우스와 함께 동작하게 되면 스크롤 바가 사라지지 않음
    • why? 마우스는 스크롤 바가 없으면 스크롤 사용이 어려움
  • scrollIndicators(.never)를 선택하면 모든 상황에서 스크롤 인디케이터가 사라짐

scrollPosition

@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를 조회할 것인지 결정한다.

ScrollTransitions

  • 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라면 원래 크기를, 그렇지 않고 이동 중이라면 조금 줄어든 크기를 적용하면 예쁜 스크롤뷰 만들 수 있음

VisualEffect Protocol

  • ScrollTransition은 VisualEffect 프로토콜을 사용해 만들어짐
    • 얘는 부모나 자식에 변경을 가하지 않고 visual appearance를 변화시키는 프로토콜
    • 직접 준수하지 않고 모디파이어를 통해 구현함
  • 이 프로토콜은 레이아웃의 기능을 안전하게 사용할 수 있는 뷰 content에 대한 사용자 정의 설정을 제공함
    • scaleEffect, rotationEffect, offset 등이 이에 속함
  • font등 전체적인 ScrollView content 크기를 변화시킬 수 있는 모디파이어는 사용 불가

요약

  • contentMargin
  • scrollTargetBehavior
  • containerRelativeFrame
  • scrollPosition
  • scrollTransition
Clone this wiki locally