Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[승용] 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

AltStyle によって変換されたページ (->オリジナル) /