自适应动态头部功能区 Animated Sticky Header Search Bar

自适应动态头部功能区 Animated Sticky Header
App 首页顶部菜单栏动态展示功能菜单

本段Demo代码实现App首页在上滑下拉时自适应更新顶部功能区的UI布局,区分默认加载时以显眼的大图标展现,在上滑后,顶部功能区收缩并缩小重新布局功能按钮,代码附在文末。

//
//  ContentView.swift
//  DynamicStickyHeader
//  动态头部功能区
//  Created by SwiftUI 100.
//

import SwiftUI

struct ContentView: View {
    
    @State var offsetY: CGFloat = 0
    @State var showSearchBar: Bool = true
    
    
    var body: some View {
        
        GeometryReader { proxy in
            let safeAreaTop = proxy.safeAreaInsets.top
            
            ScrollView(.vertical, showsIndicators: false) {
                
                VStack {
                    header(safeAreaTop: safeAreaTop)
                        .offset(y: -offsetY)
                        .zIndex(1)
                    
                    // 数据流
                    ForEach(1...10, id: \.self) { _ in
                        RoundedRectangle(cornerRadius: 10)
                            .fill(.gray.opacity(0.45))
                            .frame(width: proxy.size.width - 16, height: 180)
                    }
                    .zIndex(0)
                }
                .offset(coordinateSpace: .named("Push")) { offset in
                    offsetY = offset
                    showSearchBar = -offsetY > 80 && showSearchBar
                }
            }
            .coordinateSpace(name: "Push")
            .edgesIgnoringSafeArea(.top)
        }
        
        
    }
    
    @ViewBuilder
    func header(safeAreaTop: CGFloat) -> some View {
        
        let progress = -(offsetY / 80) > 1 ? -1 : (offsetY > 0 ? 0 : offsetY/80)
        
        VStack {
            HStack(spacing: 15) {
                HStack(spacing: 8) {
                    Image(systemName: "magnifyingglass")
                        .foregroundColor(.white)
                    
                    TextField("搜索", text: .constant(""))
                        .tint(.white)
                }
                .padding(.vertical, 10)
                .padding(.horizontal, 15)
                .background(RoundedRectangle(cornerRadius: 10, style: .continuous).fill(.black).opacity(0.15))
                .opacity(showSearchBar ? 1 : 1 + progress)
                
                Button {
                    
                } label: {
                    Image("avatar")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width:  35, height: 35)
                        .clipShape(Circle())
                        .background(Circle().fill(.white).padding(-2))
                }
                .opacity(showSearchBar ? 0 : 1)
                .overlay {
                    if showSearchBar {
                        Button{
                            showSearchBar = false
                        } label: {
                            Image(systemName: "xmark")
                                .font(.title3)
                                .fontWeight(.semibold)
                                .foregroundColor(.white)
                        }
                    }
                }
            }
            
            HStack(spacing: 0) {
                customerButton(symbolImage: "handbag.fill", title: "购物车") {
                    
                }
                
                customerButton(symbolImage: "creditcard.fill", title: "卡包") {
                    
                }
                
                
                customerButton(symbolImage: "list.bullet.clipboard.fill", title: "账单") {
                    
                }
                
                customerButton(symbolImage: "stopwatch.fill", title: "记录") {
                    
                }
            }
            .padding(.horizontal, -progress*50)
            .padding(.top, 10)
            .offset(y: progress * 65)
            .opacity(showSearchBar ? 0 : 1)
        }
        .overlay(alignment: .topLeading) {
            if !showSearchBar {
                Button{
                    showSearchBar = true
                } label: {
                    Image(systemName: "magnifyingglass")
                        .font(.title3)
                        .fontWeight(.semibold)
                        .foregroundColor(.blue)
                        .offset(x: 13, y: 8)
                        
                }
                .opacity(-progress)
                
            }
        }
        .animation(.easeOut(duration: 0.2), value: showSearchBar)
        .environment(\.colorScheme, .dark)
        .padding([.horizontal, .bottom], 15)
        .padding(.top, safeAreaTop)
        .background(
            Rectangle()
                .fill(.red.gradient)
                .padding(.bottom, -progress * 85)
        )
    }
    
    @ViewBuilder
    func customerButton(symbolImage: String, title: String, onClick: @escaping() -> ()) -> some View {
        
        let progress = -(offsetY / 40) > 1 ? -1 : (offsetY > 0 ? 0 : offsetY/40)
        
        Button {
            
        } label: {
            VStack(spacing: 8) {
                Image(systemName: symbolImage)
                    .fontWeight(.semibold)
                    .foregroundColor(.red)
                    .frame(width: 35, height: 35)
                    .background(
                        RoundedRectangle(cornerRadius: 8, style: .continuous)
                            .fill(.white)
                    )
                Text(title)
                    .font(.caption)
                    .foregroundColor(.white)
                    .fontWeight(.semibold)
                    .lineLimit(1)
            }
            .opacity(1 + progress)
            .frame(maxWidth: .infinity)
            .overlay {
                Image(systemName: symbolImage)
                    .font(.title3)
                    .fontWeight(.semibold)
                    .foregroundColor(.white)
                    .opacity(-progress)
                    .offset(y: -4)
            }
            
        }
        .frame(maxWidth: .infinity)
        
    }
}



struct OffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
    
    
}

extension View {
    
    @ViewBuilder
    func offset(coordinateSpace: CoordinateSpace, completion: @escaping(CGFloat)->()) -> some View {
        self
            .overlay {
                GeometryReader { proxy in
                    let minY = proxy.frame(in: coordinateSpace).minY
                    Color.clear
                        .preference(key: OffsetKey.self, value: minY)
                        .onPreferenceChange(OffsetKey.self) { value in
                            completion(value)
                        }
                }
            }
    }
}

  本文自 https://www.codeun.com 发布,相应代码均自主编写并严格审阅和测试,完整代码中包含丰富的学习笔记和使用方式、实用技巧。
  · 如若转载,请注明出处:https://www.codeun.com/archives/809.html ·

(0)
上一篇 2023-04-12
下一篇 2022-12-02

发表回复

登录后才能评论