디바이스는 화면 크기 이상의 것들을 하고 싶고, 그 요구사항을 다루는 방법 중 하나가 스크롤이다.
SwiftUI는 앱에 스크롤을 넣을 수 있는 몇가지 방법을 제공하는데, 그 중 하나가 ScrollView다.
ScrollView: 컨텐츠에 스크롤을 넣어주는 구성 블록
스크롤 방향을 정해주는 축을 가진다.
컨텐츠를 가진다.
컨텐츠 안에서 스크롤이 된 위치 값을 content offset이라고 한다.
struct Item: Identifiable {
var id: Int
}
struct ContentView: View {
@State var items: [Item] = (0 ..< 25).map { Item(id: $0) }
var body: some View {
ScrollView(.vertical) {
LazyVStack {
ForEach(items) { item in
ItemView(item: item)
}
}
}
}
}
struct ItemView: View {
var item: Item
var body: some View {
Text(item, format: .number)
.padding(.vertical)
.frame(maxWidth: .infinity)
}
}
Margins and safe area
시작
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
GalleryHeroView(palette: palette)
}
}
}
scrollView 자체에 패딩 넣기
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
GalleryHeroView(palette: palette)
}
}
}
.padding(.horizontal, hMargin)
safeArea에 패딩넣기(new)
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
GalleryHeroView(palette: palette)
}
}
}
.safeAreaPadding(.horizontal, hMargin)
컨텐츠별로 다르게 마진주기(new)
ScrollView {
// content
}
.contentMargins(
.vertical: 50.0,
for: .scrollContent
)
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
GalleryHeroView(palette: palette)
}
}
}
.contentMargins(.horizontal, hMargin)
Targets and positions
기본적으로 ScrollView는 스크롤 속도에 따라서 표준적인 감속도를 사용해서 스크롤이 끝나는 지점을 계산한다.
하지만 이게 중요할 때가 있다. 그래서 SwiftUI는 이 계산로직을 변경할 수 있는 ScrollTargetBehavior modifier를 추가했다.
ScrollTargetBehavior 프로토콜을 채택하는 타입을 인자로 받는다.
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
GalleryHeroView(palette: palette)
}
}
}
.contentMargins(.horizontal, hMargin)
.scrollTargetBehavior(.paging)
기본 제공 behavior
scrollTargetLayout: 레이아웃 컨테이너에 적용해서 내부의 개별 뷰들을 모두 scrollTarget이 되도록 해준다.
ScrollView(.horizontal) {
LazyHStack(spacing: hSpacing) {
ForEach(palettes) { palette in
GalleryHeroView(palette: palette)
}
}.scrollTargetLayout()
}
.contentMargins(.horizontal, hMargin)
.scrollTargetBehavior(.viewAligned)
scrollTarget: 해당뷰를 ScrollView가 염두에 두도록 해준다.
CustomBehavior
SwiftUI는 스크롤이 끝나는 곳을 판단하기 위해서 이 메소드를 호출한다.
하지만 스크롤뷰 자체가 크기가 변하는 등의 경우도 이 메소드가 호출된다.
ex. 갤러리
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
컨테이너 크기에 따라서 뷰의 크기를 조절해줄 수 있다.
HeroColrStack(palette: palette)
.frame(height: 250.0)
.containerRelativeFrame(.horizontal) // 가로 크기는 컨테이너와 일치
HeroColrStack(palette: palette)
.frame(height: 250.0)
.containerRelativeFrame(
.horizontal,
count: 2,
spacing: 10.0) // 2칸 들어가는 그리드 형태
// sizeClass로 구분
// 요 environment 값도 이제 모든 플랫폼에서 사용가능
@Environment(\\.horizontalSizeClass) private var sizeClass
HeroColrStack(palette: palette)
.aspectRatio(16.0 / 9.0, contentMode: .fit) // 비율로 프레임 설정
.containerRelativeFrame(
.horizontal,
count: sizeClass == .regular ? 2 : 1,
spacing: 10.0) // 2칸 들어가는 그리드 형태
scrollPosition
scrollIndicator modifier를 쓰면 indicator를 숨길 수 있다.
기존에도 존재하던 API긴 했지만, 맥에서는 마우스로는 스크롤바가 없으면 스크롤이 거의 불가능하기 때문에 hidden옵션을 주면 마우스를 쓰는 경우에는 뜬다.
never를 줘버리면 이 경우도 안뜨게 할 수 있지만, 스크롤을 할 수 있는 다른 수단이 필요하다.
기존에는 ScrollViewReader를 썼겠지만, 이제는 scrollPosition modifier를 쓴다.
@State private var mainID; Palette.ID? = nil
VStack {
GallerySectionHeader(mainID: $mainID)
ScrollView(.horizontal) { ... }
.scrollPosition(id: $mainID)
}
// GallertySectionHeader
GalleryPaddle(edge: .leading) {
mainID = previousID()
}
Scroll transitions
스크롤뷰에서의 위치에 따라서 뷰를 변경하고 싶다.
transition: 뷰가 나타나거나 사라질 때 뷰에 적용되어야 하는 변경사항들을 나타낸것
스크롤뷰에서의 transtion도 일반적인 transition과 거의 비슷하다.
이 phase를 보고 뷰에 transtion을 줄 수 있는 scrollTransition modifier가 추가 되었다.
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
)
.rotationEffect(
.degrees(phase.isIdentity ? 0.0: 90.0)
)
.offset(
x: phase.isIdentity ? 0.0 : 20.0,
y: phase.isIdentity ? 0.0 : 20.0
)
}