구조
구조는 단순하다. ScrollView 내에 상단에 이미지를 표시할 여백과 내용을 표시할 VStack 등의 View가 존재한다.
구현
ContentList
struct ContentList: View {
var body: some View {
VStack(alignment: .leading) {
ForEach(0..<10) { _ in
RoundedRectangle(cornerRadius: 4, style: .continuous)
.frame(width: 120, height: 25)
VStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4, style: .continuous)
.frame(width: .random(in: 150...300), height: 20)
RoundedRectangle(cornerRadius: 4, style: .continuous)
.frame(width: .random(in: 150...300), height: 20)
RoundedRectangle(cornerRadius: 4, style: .continuous)
.frame(width: .random(in: 150...300), height: 20)
}
.opacity(0.3)
}
.padding(.horizontal)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top)
}
}
임시로 사용한 ContentList는 RoundRectangle을 사용한 10개의 VStack으로 구성했다.
조금 더 역동적인 느낌을 주기 위해 너비를 150~300 사이의 임의의 값을 사용했다.
ParallaxEffect
struct ParallaxEffect: View {
var body: some View {
GeometryReader {
let offsetY = $0.frame(in: .global).minY
let isScrolled = offsetY > 0
Spacer()
.frame(height: isScrolled ? 400 + offsetY : 400)
.background {
Image("sample")
.resizable()
.scaledToFill()
.offset(y: isScrolled ? -offsetY : 0)
.blur(radius: isScrolled ? offsetY / 20 : 0)
}
}
.frame(height: 400)
}
}
GeometryReader를 사용해 스크린 기준의 좌표를 불러오고, y 축 좌표를 offsetY로 지정한다.
이후 이 offsetY가 0보다 커지는 경우 isScrolled에 반영하는데, 스크롤을 올리면 양수로 내리면 음수로 반응한다.
상단에 표시할 이미지는 Spacer의 background에 배치한다.
scledToFill을 사용하고 있기 때문에 스크롤 동작에서 offsetY가 Spacer의 frame에 더해지며 확대되는 것 같은 효과가 적용된다.
.offset(y: isScrolled ? -offsetY : 0)
또한 offsetY가 background로 사용되는 Image의 offset에도 적용되기 때문에 이미지는 항상 늘어난 Spacer의 중앙에 위치할 수 있게 된다.
왼쪽이 offset을 적용하지 않은 상태, 오른쪽은 적용된 상태다.
하단의 ContentList를 침범하지 않으며 의도한 대로의 역할을 하고 있다.
개선
지금 상태로는 Blur의 효과 때문에 상단에 미세하게 테두리가 생기는 문제가 있다.
.scaleEffect(isScrolled ? offsetY / 1000 + 1 : 1)
sclaeEffect를 살짝 적용해서 이를 해결할 수 있는데 효과는 다음과 같다.
왼쪽이 전, 오른쪽이 후이다.
완성된 코드는 다음과 같다.
struct ParallaxEffect: View {
var body: some View {
GeometryReader {
let offsetY = $0.frame(in: .global).minY
let _ = print(offsetY)
let isScrolled = offsetY > 0
Spacer()
.frame(height: isScrolled ? 400 + offsetY : 400)
.background {
Image("sample")
.resizable()
.scaledToFill()
.offset(y: isScrolled ? -offsetY : 0)
.scaleEffect(isScrolled ? offsetY / 1000 + 1 : 1)
.blur(radius: isScrolled ? offsetY / 20 : 0)
}
}
.frame(height: 400)
}
}
+
주의사항으로는 상단에 표시하는 사진의 비율이 정방형이어야 한다.
scaledToFill을 사용하기 때문에 종횡비가 일치하지 않는다면 아래 표시되는 ContentList의 영역을 침범한다.
물론, ContentList에 흰색 배경을 추가하면 해결할 수 있긴 하다.
'학습 노트 > UIBreaking (2023)' 카테고리의 다른 글
AutoScrolling #06 (0) | 2023.04.07 |
---|---|
AutoScrolling #05 (0) | 2023.04.04 |
AutoScrolling #04 (0) | 2023.03.23 |
AutoScrolling #03 (0) | 2023.03.23 |
AutoScrolling #02 (0) | 2023.03.22 |