本段Demo代码实现App首页在上滑下拉时自适应更新顶部功能区的UI布局,区分默认加载时以显眼的大图标展现,在上滑后,顶部功能区收缩并缩小重新布局功能按钮,代码附在文末。
– Xcode 14
– SwiftUI 4.0
//
// 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)
}
}
}
}
}
参考来源 @Kavsoft
本文自 https://www.codeun.com 发布,相应代码均自主编写并严格审阅和测试,完整代码中包含丰富的学习笔记和使用方式、实用技巧。
· 如若转载,请注明出处:https://www.codeun.com/archives/809.html ·