-
[SwiftUI] 스크롤 방향에 반응하는 헤더 UISwift/끄적끄적 2025. 8. 17. 19:49
이번에 디자인 요구사항에 스크롤 방향에 따라 헤더를 보여주는 작업을 해야 했음!
보통 글을 읽을 때 맨 위에 버튼을 누르기 위해서 다시 위로 가는 경우가 있는데
편하게 스크롤만 올려주면 생기도록 해주는 거임!

위 예시처럼 네이버 블로그 상단 탭(헤더)이 스크롤의 방향에 따라 보이거나 사라지거나
이걸 구현해 보겠음!
우선 스크롤 감지하는 코드부터 구현하겠음
[전체 코드]
struct HeaderDemo: View { @State private var directionText = "스크롤 방향" @State private var lastY: CGFloat = 0 private let headerH: CGFloat = 56 private let directionThreshold: CGFloat = 1.0 var body: some View { ZStack(alignment: .top) { ScrollView { GeometryReader { proxy in let y = proxy.frame(in: .named("scroll")).minY Color.clear .frame(height: 0) .onAppear { lastY = y } .onChange(of: y) { oldY, newY in let dy = newY - oldY if dy < -directionThreshold { directionText = "⬇️ down" } else if dy > directionThreshold { directionText = "⬆️ up" } } } .frame(height: 0) VStack { ForEach(0...30, id: \.self) { i in RoundedRectangle(cornerRadius: 12) .fill(Color.green) .frame(height: 64) .overlay(Text("Row \(i)")) .padding(.horizontal) } } .padding(.top, headerH) } .coordinateSpace(name: "scroll") HStack { Text("테스트") .font(.title2) Spacer() } .padding(.horizontal, 20) .frame(height: headerH) .frame(maxWidth: .infinity) .background(.ultraThinMaterial) VStack { HStack { Spacer() Text(directionText) .padding() } Spacer() } } } }
[코드 설명]
ScrollView { GeometryReader { proxy in let y = proxy.frame(in: .named("scroll")).minY Color.clear .frame(height: 0) .onAppear { lastY = y } .onChange(of: y) { oldY, newY in let dy = newY - oldY if dy < -directionThreshold { directionText = "⬇️ down" } else if dy > directionThreshold { directionText = "⬆️ up" } } } .frame(height: 0) . . . (중략) } .coordinateSpace(name: "scroll")여기서 중요한 것은 GeometryReader라는 측정용 컨테이너임
GeometryReader는 화면에 투명한 상자를 하나 놓고,
그 상자가 현재 어디에 있고, 얼마나 큰지를 실시간으로 알려줄 수 있다고 보면 됨!
기본적으로 이 상자는 자기에게 허용된 공간을 최대한 차지하려고 해서
센서로 사용할 댄 'frame(height:0)'같은 제한을 걸어서 쓸 수 있음
이 코드에서는 proxy로 GeometryReader의 측정 결과를 받아 사용되는데,
proxy.frame(in:)은 '어떤 기준 지도'에서 이 상자의 위치/크기를 알려달라는 뜻임!
.local, .global 같은 지도가 있는데 여기선 .name("scroll")을 사용해서
하나의 지도만 측정할 수 있도록 한 거임!
스크롤의 값을 안정적으로 받기 위해. coordinateSpace(name:)을 사용해서
지도의 기준을 지정하는 코드와 함께 씀!
1.6666666666666643 1.3333333333333357 1.3333333333333357 1.0 1.0 0.6666666666666643 0.6666666666666643 0.0 0.0 -0.3333333333333357 -0.3333333333333357 -9.333333333333336 -9.333333333333336 -19.333333333333336 -19.333333333333336 -28.0 -28.0 -29.666666666666664 -29.666666666666664 -30.666666666666668 -30.666666666666668 -36.66666666666667실제로 스크롤을 움직이면 oldY, newY의 값이 이렇게 변화되는 걸 확인할 수 있음 (print 적용)
이 값을 이용해서 두 값의 차이가(newY - oldY)
음수로 가고 있으면 아래 스크롤, 양수면 위로 스크롤로 판단이 되는 것!
이제 스크롤이 올라가고 내려가는 것을 알았으니 헤더 부분을 나타났다가 사라지는 동작을 구현해 보겠음!
[전체 코드]
struct HeaderDemo: View { @State private var directionText = "스크롤 방향" @State private var lastY: CGFloat = 0 @State private var headerHidden = false private let headerH: CGFloat = 56 private let nearTopThreshold: CGFloat = -50 // 맨 위 근처면 항상 표시 private let directionThreshold: CGFloat = 1.0 var body: some View { ZStack(alignment: .top) { ScrollView { GeometryReader { proxy in let y = proxy.frame(in: .named("scroll")).minY Color.clear .frame(height: 0) .onAppear { lastY = y } .onChange(of: y) { oldY, newY in let dy = newY - oldY if newY > nearTopThreshold { headerHidden = false directionText = "스크롤 방향" } else if dy < -directionThreshold { headerHidden = true // 내려가는 중 directionText = "⬇️ down" } else if dy > directionThreshold { headerHidden = false // 올라가는 중 directionText = "⬆️ up" } } } .frame(height: 0) VStack { ForEach(0...30, id: \.self) { i in RoundedRectangle(cornerRadius: 12) .fill(Color.green) .frame(height: 64) .overlay(Text("Row \(i)")) .padding(.horizontal) } } .padding(.top, headerH) } .coordinateSpace(name: "scroll") HStack { Text("테스트") .font(.title2) Spacer() } .padding(.horizontal, 20) .frame(height: headerH) .frame(maxWidth: .infinity) .background(.ultraThinMaterial) .offset(y: headerHidden ? -(headerH + 8) : 0) // 위로 숨김 .opacity(headerHidden ? 0 : 1) .animation(.easeInOut(duration: 0.2), value: headerHidden) VStack { HStack { Spacer() Text(directionText) .padding() } Spacer() } } .background(Color(.systemBackground)) } }
[코드 설명]
.onChange(of: y) { oldY, newY in let dy = newY - oldY if newY > nearTopThreshold { headerHidden = false directionText = "스크롤 방향" } else if dy < -directionThreshold { headerHidden = true // 내려가는 중 directionText = "⬇️ down" } else if dy > directionThreshold { headerHidden = false // 올라가는 중 directionText = "⬆️ up" } }간단하게 onChange 부분에서 headerHidden이라는 변수를 만들어 Bool 값을 처리해 줬음
(nearTopThreshold는 맨 위에서도 미세한 스크롤에 사라지는 것을 방지하기 위해 특정 영역을 설정해 준 거임)
HStack { Text("테스트") .font(.title2) Spacer() } .padding(.horizontal, 20) .frame(height: headerH) .frame(maxWidth: .infinity) .background(.ultraThinMaterial) .offset(y: headerHidden ? -(headerH + 8) : 0) // 위로 숨김 .opacity(headerHidden ? 0 : 1) .animation(.easeInOut(duration: 0.2), value: headerHidden)그럼 hearderHidden의 Bool 값에 따라 나타나고 사라지는 것이 가능해짐!
애니메이션이나, 어떻게 사라지게 할 건지는 취향차이 ㅎ
'Swift > 끄적끄적' 카테고리의 다른 글
[SwiftUI] @Observable과 @Bindable에 대하여 (0) 2025.10.13 [SwiftUI] 더보기 영역 구현 (0) 2025.09.22 [SwiftUI] confirmationDialog 활용법 (1) 2025.09.04 CocoaPod 설치 과정 (3) 2024.10.24 [Swift] ViewController 화면전환 (2) 2023.04.22