From af398a54acb6eb5f7d72efa2b9f5291f26681026 Mon Sep 17 00:00:00 2001 From: jutamin Date: Sun, 22 Jun 2025 18:20:22 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20ShopView=20StickyHeader=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shop/View/OffsetModifier.swift | 41 ++++++++++ .../StarbucksDaisy/Shop/View/ShopView.swift | 75 ++++++++++++++++--- 2 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/OffsetModifier.swift diff --git a/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/OffsetModifier.swift b/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/OffsetModifier.swift new file mode 100644 index 0000000..a3c10dd --- /dev/null +++ b/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/OffsetModifier.swift @@ -0,0 +1,41 @@ +// +// OffsetModifier.swift +// StarbucksDaisy +// +// Created by 원주연 on 6/22/25. +// + +import Foundation +import SwiftUI + +struct OffsetModifier: ViewModifier { + @Binding var offset: CGFloat + + var returnromStart: Bool = true + @State var startValue: CGFloat = 0 + + func body(content: Content) -> some View { + content + .overlay(content: { + GeometryReader(content: { proxy in + Color.clear + .preference(key: OffsetKey.self, value: proxy.frame(in: .named("SCROLL")).minY) + .onPreferenceChange(OffsetKey.self) { value in + if startValue == 0 { + startValue = value + } + + offset = (value - (returnromStart ? startValue : 0)) + } + }) + }) + } +} + +struct OffsetKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() + } +} diff --git a/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/ShopView.swift b/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/ShopView.swift index e9b8da0..6f1cb32 100644 --- a/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/ShopView.swift +++ b/daisy/StarbucksDaisy/StarbucksDaisy/Shop/View/ShopView.swift @@ -10,29 +10,36 @@ import SwiftUI struct ShopView: View { /// Best Items 섹션의 현재 페이지 상태를 추적 @State private var currentBestItemPage = 0 + @State var headerOffsets: (CGFloat, CGFloat) = (0, 0) var viewModel: ShopViewModel = .init() let columns = Array(repeating: GridItem(.flexible()), count: 2) var body: some View { ScrollView { - VStack(spacing: 20) { - TopBanners - AllProducts - BestItems - NewProducts - } + headerView() + LazyVStack(spacing: 20, pinnedViews: [.sectionHeaders], content: { + Section(content: { + TopBanners + AllProducts + BestItems + NewProducts + }, header: { + pinnedHeaderView() + .modifier(OffsetModifier(offset: $headerOffsets.0, returnromStart: false)) + .modifier(OffsetModifier(offset: $headerOffsets.1)) + }) + }) } .padding(.horizontal, 16) + .safeAreaPadding(.bottom, 90) + .ignoresSafeArea() .background(.white01) .scrollIndicators(.hidden) + .coordinateSpace(name: "SCROLL") } private var TopBanners: some View { - VStack(alignment: .leading, spacing: 16) { - Text("Starbucks Online Store") - .font(.mainTextBold24) - ScrollView(.horizontal) { LazyHStack(spacing: 28) { Image("shopBanner1") @@ -42,8 +49,6 @@ struct ShopView: View { } } .scrollIndicators(.hidden) - } - .padding(.top, 27) } private var AllProducts: some View { @@ -146,6 +151,52 @@ struct ShopView: View { .frame(maxHeight: 446) } } + + @ViewBuilder + private func headerView() -> some View { + GeometryReader { proxy in + let minY = proxy.frame(in: .named("SCROLL")).minY + let size = proxy.size + let height = max(0, size.height + minY) + + Rectangle() + .fill(Color.white01) + .frame(width: size.width, height: height, alignment: .top) + .offset(y: -minY) + } + .frame(height: 27) + } + + @ViewBuilder + private func pinnedHeaderView() -> some View { + + let threshhold = -(getScreenSize().height * 0.05) + + HStack { + if headerOffsets.0 < threshhold { + Spacer() + } + + Text("Starbucks Online Store") + .font(headerOffsets.0 < threshhold ? .mainTextBold16 : .mainTextBold24) + .animation(.easeInOut(duration: 0.2), value: headerOffsets.0) + + Spacer() + + } + .frame(height: 90, alignment: .bottomLeading) + .safeAreaPadding(.bottom, headerOffsets.0 < threshhold ? 16 : 0) + .background(Color.white01) + } +} + +extension View { + func getScreenSize() -> CGSize { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { + return .zero + } + return windowScene.screen.bounds.size + } } #Preview {