본문 바로가기

학습 노트/UIBreaking (2023)

ParallaxEffect

구조

구조는 단순하다. 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