이번에 정리해 볼 것은 네이버 웹툰의 UI에서 스크롤시 내려오는 header를 구현하고 상단 배너 밑의 수평으로 된 스크롤뷰를 구현해 본 것을 정리하고자 한다.
일단 구현한 최종 결과물의 이미지이다.
먼저 수평 스크롤 뷰의 구현부터 정리하고자 한다.
해당 이미지는 구현 코드이다.
간단하게 스크롤뷰를 수평으로 생성하고 해당하는 셀들을 따로 구현해서 결과물을 만들었다.
다음으로는 스크롤 기능 구현 코드이다.
먼저 네이버 웹툰의 스크롤 UI를 보면 스크롤 시 상단에서 hearder가 내려와 정확하게 상단 배너 뷰의 하단과 맞춰지고 계속 스크롤을 하다 보면 header의 밑에 요일뷰들이 적혀 있는 것을 알 수 있다.
내 얕은 지식으로의 구현방법을 생각해본다면 스크롤 뷰의 y offset의 값을 가지고 지정된 offset의 위치가 된다면 상단에서 header가 내려오고 특정부분에서는 요일뷰가 나타난다고 생각을 하여 일단은 이런식으로 구현을 해보았지만 다음에 구현할 grid뷰 부분에서 바뀔수도 있다고 생각한다.
스크롤 뷰를 구현해 보면서 스크롤뷰는 따로 offset 값을 주지 않아서 직접 구현을 해야했는데 여러가지 방법이 있었지만 내가 선택한 방법은 PreferenceKey이다.
offset을 알기 위해서는 4가지의 작업이 필요했다.
먼저 PreferenceKey를 사용한 offsetkey의 구조체를 선언한다.
그리고 이 방법은 보이지 않는 뷰를 그려 해당 offset과의 거리를 기반으로 scrollview의 offset을 그리는 방법 같았다.
그리고 해당 함수를 스크롤 뷰 안에 선언하고 scrollview에는 이름을 지어 연결하는 방식 같았다.
이렇게 스크롤뷰의 offset을 구했다면 해당 offset의 값으로 구현을 하면 된다.
스크롤을 해도 계속 header가 남아있어야 하니까. 나는 overlay로 스크롤뷰위에 overlay하여 상단의 red view는 padding 함수를 만들어 자연스럽게 내려오도록 구현하였고 밑의 파란색 뷰는 특정 offset이 된다면 oppacity를 사용하여 나타나도록 구현하였다.
수평 스크롤뷰 전체 코드
import SwiftUI
struct naver_webtoon_second: View {
@Environment(\.colorScheme) var colorScheme
@State var second_color_list : [Color] = [.red, .green, .blue, .yellow, .purple, .brown]
@State var sceond_title_list : [String] = ["박부장", "스톰브레이커", "운석귀환", "재벌집 막내 조카", "역대급 영지 건축가", "0.1초"]
var body: some View {
GeometryReader{proxy in
ZStack(alignment: .leading){
if(colorScheme == .dark){
Color.init(cgColor: CGColor(red: 34/255, green: 34/255, blue: 34/255, alpha: 1))
}
else{
Color.white
}
VStack(alignment: .leading){
HStack{
Text("지금! 인기 급상승 웹툰")
Spacer()
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.gray)
.opacity(0.8)
}
ScrollView(.horizontal, showsIndicators: false){
HStack{
ForEach(0..<6){i in
second_view_cell(second_color_list: $second_color_list[i], sceond_title_list : $sceond_title_list[i])
}
}
}
}
.padding(10)
}
}
}
}
struct second_view_cell:View{
@Binding var second_color_list : Color
@Binding var sceond_title_list : String
var body: some View{
VStack{
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(second_color_list)
Text("\(sceond_title_list)")
.frame(width: 100)
.lineLimit(1)
}
}
}
struct naver_webtoon_second_Previews: PreviewProvider {
static var previews: some View {
naver_webtoon_second()
}
}
header 내려오는 전체 코드
import SwiftUI
struct sticky_header_test: View {
@State var offsetY : CGFloat = 0
var body: some View {
GeometryReader{proxy in
ScrollView(.vertical, showsIndicators: true){
VStack(spacing: 0){
Rectangle()
.frame(width: proxy.size.width, height: 2000)
.foregroundColor(.yellow)
}
.offset(coordinationSpace: .named("header")){value in
offsetY = value
let _ = print("\(offsetY)")
}
}
.coordinateSpace(name: "header")
.overlay(
VStack(spacing: 0){
Rectangle()
.frame(width: proxy.size.width, height: 100)
.foregroundColor(.red)
.padding(.top, padding_calc())
Rectangle()
.frame(width: proxy.size.width, height: 50)
.foregroundColor(.blue)
.opacity(offsetY < -150 ? 1 : 0)
}
, alignment: .top
)
}
.ignoresSafeArea(.all)
}
func padding_calc() -> CGFloat{
var result_num : CGFloat = 0
if(offsetY < -90){
result_num = 0
}else{
result_num = 100 + offsetY
print("result num \(result_num)")
}
return -result_num
}
}
struct offsetkey : PreferenceKey{
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
extension View{
@ViewBuilder
func offset_scroll(CoordinateSpace : CoordinateSpace, complation : @escaping (CGFloat)->())-> some View{
self
.overlay{
GeometryReader{proxy in
let minY = proxy.frame(in: .global).minY
Color.clear
.preference(key: OffsetKey.self, value : minY)
.onPreferenceChange(OffsetKey.self){value in
complation(value)
}
}
}
}
}
struct sticky_header_test_Previews: PreviewProvider {
static var previews: some View {
sticky_header_test()
}
}