import SwiftUI #if os(iOS) import UIKit #endif struct HomeView: View { @State var searchText = "" @State var selectedCategory = "Stores" @State var scrollOffset: CGFloat = 0 private let categories: [CategoryModel] = [ .init(title: "Stores", systemIcon: "storefront"), .init(title: "Asian", systemIcon: "fork.knife"), .init(title: "Breakfast", systemIcon: "sun.max"), .init(title: "Pizza", systemIcon: "takeoutbag.and.cup.and.straw"), .init(title: "Dessert", systemIcon: "cup.and.saucer") ] private let featuredStores: [FeaturedStoreCardModel] = [ .init( name: "Burger Kingdom", rating: 4.9, reviews: "1.2k", distance: "1.2 km", category: "Fast Food", promoText: "10% OFF", isFavorite: false, iconName: "takeoutbag.and.cup.and.straw" ), .init( name: "Sushi House", rating: 4.8, reviews: "840", distance: "2.2 km", category: "Japanese", promoText: nil, isFavorite: true, iconName: "fork.knife" ) ] private let nearbyStores: [FeaturedStoreCardModel] = [ .init( name: "Pizza Prime", rating: 4.7, reviews: "510", distance: "1.8 km", category: "Pizza", promoText: nil, isFavorite: false, iconName: "takeoutbag.and.cup.and.straw" ), .init( name: "Aoyama", rating: 4.9, reviews: "2.4k", distance: "3.1 km", category: "Japanese", promoText: nil, isFavorite: true, iconName: "fork.knife" ) ] private let specials: [SpecialOfferCardModel] = [ .init(title: "Get 50% OFF", subtitle: "For your first order", colors: [Color(hex: "#1E6B43"), Color(hex: "#58A56C")]), .init(title: "Enjoy spicy day", subtitle: "Delivery in 20 min", colors: [Color(hex: "#F04B3E"), Color(hex: "#FF8C42")]) ] private let headerExpandedHeight: CGFloat = 260 private let headerCollapsedHeight: CGFloat = 120 private let contentTopPadding: CGFloat = 296 var body: some View { let collapseProgress = clamp(value: -scrollOffset / (headerExpandedHeight - headerCollapsedHeight), lower: 0, upper: 1) let headerHeight = headerExpandedHeight - (headerExpandedHeight - headerCollapsedHeight) * collapseProgress let headerPadding = headerExpandedHeight - (headerExpandedHeight - headerCollapsedHeight) * collapseProgress return ZStack(alignment: .top) { #if os(iOS) TrackableScrollView(onOffsetChange: { value in scrollOffset = min(0, -value) }) { contentStack .padding(.top, headerPadding - 10) .padding(.bottom, 24) } .frame(maxWidth: .infinity, maxHeight: .infinity) #else ScrollView { contentStack .padding(.top, headerPadding + 16) .padding(.bottom, 24) } .coordinateSpace(name: "scroll") .onPreferenceChange(ScrollOffsetKey.self) { value in scrollOffset = min(0, value) } #endif header(collapseProgress: collapseProgress, height: headerHeight) .frame(maxWidth: .infinity, alignment: .top) } .background(AppColors.backgroundLight) .ignoresSafeArea(edges: .top) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } private var contentStack: some View { VStack(spacing: 24) { scrollOffsetMarker categoriesSection section(title: "Featured") { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(featuredStores) { store in FeaturedStoreCard(store: store) .frame(width: 190) } } .padding(.horizontal, 20) } } section(title: "#SpecialForYou") { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 16) { ForEach(specials) { item in SpecialOfferCard(model: item) .frame(width: 260, height: 120) } } .padding(.horizontal, 20) } } section(title: "Near you") { VStack(spacing: 16) { ForEach(nearbyStores) { store in FeaturedStoreCard(store: store) } } .padding(.horizontal, 20) } } } private func header(collapseProgress: CGFloat, height: CGFloat) -> some View { let titleOpacity = 1 - clamp(value: collapseProgress * 1.2, lower: 0, upper: 1) let topRowOpacity = 1 - clamp(value: collapseProgress * 1.4, lower: 0, upper: 1) return ZStack(alignment: .top) { RoundedRectangle(cornerRadius: 32, style: .continuous) .fill(AppColors.brandDark) .frame(height: height) .overlay(headerRings.opacity(1 - collapseProgress), alignment: .topTrailing) VStack(alignment: .leading, spacing: 16) { Spacer().frame(height: 20) HStack(alignment: .center, spacing: 12) { Circle() .fill(AppColors.brandSoft) .frame(width: 40, height: 40) .overlay( Image(systemName: "person.fill") .foregroundStyle(AppColors.brandDark) ) VStack(alignment: .leading, spacing: 4) { Text("DELIVERY LOCATION") .font(AppTypography.overline) .tracking(AppTypography.captionLetterSpacing) .foregroundStyle(AppColors.brandSoft) HStack(spacing: 6) { Text("Canggu, Kuta Utara, Bali") .font(AppTypography.heading3) .foregroundStyle(AppColors.textInverse) Image(systemName: "chevron.down") .font(.caption) .foregroundStyle(AppColors.brandSoft) } } Spacer() Circle() .fill(Color.white.opacity(0.18)) .frame(width: 40, height: 40) .overlay( Image(systemName: "bell") .foregroundStyle(AppColors.textInverse) ) } .opacity(topRowOpacity) .offset(y: collapseProgress * -12) Text("O que vai querer pedir hoje?") .font(AppTypography.heading1) .foregroundStyle(AppColors.textInverse) .fixedSize(horizontal: false, vertical: true) .opacity(titleOpacity) .offset(y: collapseProgress * -20) SearchBar(placeholder: "Search menu, restaurant or craving", text: $searchText) .offset(y: collapseProgress * -140) } .padding(.horizontal, 20) .padding(.top, 18) } } @ViewBuilder private var scrollOffsetMarker: some View { #if os(iOS) EmptyView() #else ScrollOffsetReader() .offset(y: -contentTopPadding) #endif } private func section(title: String, @ViewBuilder content: () -> Content) -> some View { VStack(alignment: .leading, spacing: 16) { Text(title) .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) .padding(.horizontal, 20) content() } } private var categoriesSection: some View { VStack(alignment: .leading, spacing: 12) { Text("Categories") .font(AppTypography.heading2) .foregroundStyle(AppColors.textPrimary) .padding(.horizontal, 20) ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 12) { ForEach(categories) { category in CategoryChip( title: category.title, systemIcon: category.systemIcon, isActive: category.title == selectedCategory ) .onTapGesture { selectedCategory = category.title } } } .padding(.horizontal, 20) } } } private var headerRings: some View { ZStack { Circle() .stroke(Color.white.opacity(0.08), lineWidth: 1) .frame(width: 180, height: 180) .offset(x: 40, y: -10) Circle() .stroke(Color.white.opacity(0.08), lineWidth: 1) .frame(width: 130, height: 130) .offset(x: 70, y: 10) } } } struct CategoryModel: Identifiable { let id = UUID() let title: String let systemIcon: String } struct CategoryChip: View { let title: String let systemIcon: String let isActive: Bool var body: some View { HStack(spacing: 8) { Image(systemName: systemIcon) .font(.caption) Text(title) .font(AppTypography.heading3) } .foregroundStyle(isActive ? AppColors.textInverse : AppColors.textPrimary) .padding(.horizontal, 16) .padding(.vertical, 10) .background(isActive ? AppColors.brandDark : AppColors.surface) .clipShape(Capsule()) } } struct SearchBar: View { let placeholder: String @Binding var text: String var body: some View { HStack(spacing: 12) { Image(systemName: "magnifyingglass") .foregroundStyle(AppColors.textMuted) TextField(placeholder, text: $text) .textInputAutocapitalization(.never) .autocorrectionDisabled() Spacer() Image(systemName: "slider.horizontal.3") .foregroundStyle(AppColors.textMuted) } .padding(.horizontal, 16) .frame(height: 52) .background(AppColors.surface) .clipShape(RoundedRectangle(cornerRadius: AppLayout.radiusLG, style: .continuous)) } } struct ScrollOffsetKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } struct ScrollOffsetReader: View { var body: some View { GeometryReader { proxy in Color.clear .preference(key: ScrollOffsetKey.self, value: proxy.frame(in: .named("scroll")).minY) } .frame(height: 1) } } func clamp(value: CGFloat, lower: CGFloat, upper: CGFloat) -> CGFloat { min(max(value, lower), upper) } #if os(iOS) @MainActor struct TrackableScrollView: UIViewRepresentable { let onOffsetChange: (CGFloat) -> Void let content: Content init(onOffsetChange: @escaping (CGFloat) -> Void, @ViewBuilder content: () -> Content) { self.onOffsetChange = onOffsetChange self.content = content() } func makeCoordinator() -> Coordinator { Coordinator(onOffsetChange: onOffsetChange) } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false scrollView.alwaysBounceVertical = true let host = UIHostingController(rootView: content) host.view.translatesAutoresizingMaskIntoConstraints = false host.view.backgroundColor = .clear scrollView.addSubview(host.view) NSLayoutConstraint.activate([ host.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor), host.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor), host.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), host.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), host.view.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor) ]) context.coordinator.hostingController = host scrollView.delegate = context.coordinator return scrollView } func updateUIView(_ uiView: UIScrollView, context: Context) { context.coordinator.hostingController?.rootView = content } final class Coordinator: NSObject, UIScrollViewDelegate { var hostingController: UIHostingController? let onOffsetChange: (CGFloat) -> Void init(onOffsetChange: @escaping (CGFloat) -> Void) { self.onOffsetChange = onOffsetChange } func scrollViewDidScroll(_ scrollView: UIScrollView) { onOffsetChange(scrollView.contentOffset.y) } } } #endif