feat: 设置页面动效

This commit is contained in:
jinyaqiu 2025-08-15 15:35:17 +08:00
parent f7eb4c0f51
commit 35962c644e
6 changed files with 434 additions and 357 deletions

View File

@ -1,228 +1,194 @@
import SwiftUI
//
// MARK: -
extension AnyTransition {
///
static var slideFromLeading: AnyTransition {
.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .leading).combined(with: .opacity)
insertion: .move(edge: .trailing).combined(with: .opacity), //
removal: .move(edge: .leading).combined(with: .opacity) //
)
}
}
// 1.
// MARK: -
enum Route: Hashable {
case settings
case settings //
}
// MARK: -
struct ContentView: View {
@State private var showModal = false
@State private var showSettings = false
@State private var navigationPath = NavigationPath()
@State private var contentOffset: CGFloat = 0
// MARK: -
@State private var showModal = false //
@State private var showSettings = false //
@State private var navigationPath = NavigationPath() //
@State private var contentOffset: CGFloat = 0 //
// MARK: -
var body: some View {
NavigationStack(path: $navigationPath) {
// NavigationStack
//
let _ = Self._printChanges()
let _ = print("Navigation path changed: \(navigationPath)")
let _ = print("导航路径已更新: \(navigationPath)")
//
ZStack {
//
VStack {
VStack(spacing: 20) {
// This spacer ensures content stays below the status bar
//
Spacer().frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
//
// -
HStack {
Spacer()
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = true
}
}) {
//
Button(action: showUserProfile) {
Image(systemName: "gearshape")
.font(.title2)
.padding()
}
Spacer() //
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.offset(x: showModal ? UIScreen.main.bounds.width * 0.35 : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showModal)
.edgesIgnoringSafeArea(.all)
}
//
if showModal {
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = false
}
}
.transition(.opacity)
}
// Modal with animation - will be pushed off-screen by SettingsView
SlideInModal(isPresented: $showModal, onDismiss: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showModal = false
}
}) {
// Modal content
// Modal content with offset for SettingsView
VStack(spacing: 20) {
//
HStack(alignment: .center, spacing: 16) {
//
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
//
List {
Section(header: Text("我的收藏")) {
ForEach(1...5, id: \.self) { item in
HStack {
Image(systemName: "photo")
.foregroundColor(.blue)
.clipShape(Circle())
.frame(width: 40, height: 40)
.background(Color.blue.opacity(0.1))
.cornerRadius(8)
// ID
VStack(alignment: .leading, spacing: 4) {
Text("用户名")
Text("项目 \(item)")
.font(.headline)
.foregroundColor(.primary)
Text("ID: 12345678")
Text("这是第\(item)个项目的描述")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 16)
VStack(alignment: .leading, spacing: 8) {
Text("会员等级")
.font(.headline)
.foregroundColor(.primary)
Text("会员时间")
.font(.subheadline)
.foregroundColor(.secondary)
Text("会员中心")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color(red: 0.92, green: 0.92, blue: 0.92))
.cornerRadius(10)
.padding(.horizontal, 16)
VStack(spacing: 12) {
// memories
Button(action: {
print("memories")
}) {
HStack(spacing: 16) {
Image(systemName: "crown.fill")
.foregroundColor(.orange)
.frame(width: 24, height: 24)
Text("My Memories")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// Box
Button(action: {
print("Box")
}) {
HStack(spacing: 16) {
Image(systemName: "clock.fill")
.foregroundColor(.blue)
.frame(width: 24, height: 24)
Text("My Bind Box")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// setting
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showSettings = true
}
}) {
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.foregroundColor(.purple)
.frame(width: 24, height: 24)
Text("Setting")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(10)
.contentShape(Rectangle()) // 使
.padding(.vertical, 4)
}
.buttonStyle(PlainButtonStyle()) //
}
.padding(.horizontal, 16)
//
Section(header: Text("最近活动")) {
ForEach(6...10, id: \.self) { item in
HStack {
Image(systemName: "clock")
.foregroundColor(.orange)
.frame(width: 40, height: 40)
.background(Color.orange.opacity(0.1))
.cornerRadius(8)
VStack(alignment: .leading, spacing: 4) {
Text("活动 \(item)")
.font(.headline)
Text("\(item)分钟前更新")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.vertical, 8)
}
// Apply offset to the entire modal when SettingsView is shown
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showSettings)
Text("查看")
.font(.caption)
.padding(6)
.background(Color.blue.opacity(0.1))
.foregroundColor(.blue)
.cornerRadius(4)
}
.padding(.vertical, 4)
}
}
}
.listStyle(GroupedListStyle())
.padding(.top, 0)
ZStack {
// Semi-transparent overlay for settings
if showSettings {
Color.black.opacity(0.4)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
//
.offset(x: showModal ? UIScreen.main.bounds.width * 0.8 : 0)
//
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showModal)
.edgesIgnoringSafeArea(.all)
.onTapGesture {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showSettings = false
}
//
SlideInModal(
isPresented: $showModal,
onDismiss: hideUserProfile
) {
UserProfileModal(
showModal: $showModal,
showSettings: $showSettings
)
}
//
.offset(x: showSettings ? UIScreen.main.bounds.width : 0)
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)
//
ZStack {
//
if showSettings {
Color.black.opacity(0)
.edgesIgnoringSafeArea(.all)
.onTapGesture(perform: hideSettings)
.transition(.opacity)
}
// Full screen settings view with slide animation
//
if showSettings {
SettingsView(isPresented: $showSettings)
.transition(.move(edge: .leading))
.zIndex(1) // Ensure it's above other content
.onAppear {
// Reset the navigation path when settings appear
.transition(.move(edge: .leading)) //
.zIndex(1) //
.onAppear(perform: resetNavigationPath)
}
}
.animation(.spring(response: 0.6, dampingFraction: 0.8), value: showSettings)
}
}
}
// MARK: -
///
private func showUserProfile() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
showModal.toggle()
}
}
///
private func hideUserProfile() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
showModal = false
}
}
///
private func hideSettings() {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8)) {
showSettings = false
}
}
///
private func resetNavigationPath() {
navigationPath.removeLast(navigationPath.count)
}
}
}
.animation(.spring(response: 0.5, dampingFraction: 0.8), value: showSettings)
}
}
}
}
// MARK: -
#Preview {
ContentView()
}

View File

@ -0,0 +1,22 @@
import SwiftUI
// MARK: -
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners,
cornerRadii: CGSize(width: radius, height: radius)
)
return Path(path.cgPath)
}
}
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}

View File

@ -16,8 +16,8 @@ struct SlideInModal<Content: View>: View {
ZStack(alignment: .leading) {
//
if isPresented {
Color.black
.opacity(0.5)
Color.clear
.contentShape(Rectangle())
.edgesIgnoringSafeArea(.all)
.transition(.opacity)
.zIndex(1)
@ -49,7 +49,7 @@ struct SlideInModal<Content: View>: View {
.animation(animation, value: isPresented)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.edgesIgnoringSafeArea(.all)
}
}

View File

@ -0,0 +1,135 @@
import SwiftUI
struct UserProfileModal: View {
@Binding var showModal: Bool
@Binding var showSettings: Bool
var body: some View {
// Modal content with transparent background
VStack(spacing: 20) {
Spacer()
.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
//
HStack(alignment: .center, spacing: 16) {
//
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 60, height: 60)
.foregroundColor(.blue)
.clipShape(Circle())
// ID
VStack(alignment: .leading, spacing: 4) {
Text("用户名")
.font(.headline)
.foregroundColor(.primary)
Text("ID: 12345678")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 16)
VStack(alignment: .leading, spacing: 8) {
Text("会员等级")
.font(.headline)
.foregroundColor(.primary)
Text("会员时间")
.font(.subheadline)
.foregroundColor(.secondary)
Text("会员中心")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(Color.clear)
.padding(.horizontal, 16)
VStack(spacing: 12) {
// memories
Button(action: {
print("memories")
}) {
HStack(spacing: 16) {
Image(systemName: "crown.fill")
.foregroundColor(.orange)
.frame(width: 24, height: 24)
Text("My Memories")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// Box
Button(action: {
print("Box")
}) {
HStack(spacing: 16) {
Image(systemName: "clock.fill")
.foregroundColor(.blue)
.frame(width: 24, height: 24)
Text("My Bind Box")
.font(.headline)
.foregroundColor(.primary)
Spacer()
}
.padding()
.cornerRadius(10)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
// setting
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
showSettings = true
}
}) {
HStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.foregroundColor(.purple)
.frame(width: 24, height: 24)
Text("Setting")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
}
.padding()
.background(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
)
.contentShape(Rectangle()) // 使
}
.buttonStyle(PlainButtonStyle()) //
}
.padding(.horizontal, 16)
Spacer()
}
.frame(width: UIScreen.main.bounds.width * 0.8)
.background(Color(red: 0.87, green: 0.87, blue: 0.87))
.cornerRadius(20, corners: [.topRight, .bottomRight])
.edgesIgnoringSafeArea(.all)
}
}
#Preview {
UserProfileModal(showModal: .constant(true), showSettings: .constant(false))
}

View File

@ -1,153 +1,33 @@
import SwiftUI
///
struct SettingsView: View {
// MARK: -
/// - dismiss
@Environment(\.dismiss) private var dismiss
@State private var isAppeared = false
/// - /
@Binding var isPresented: Bool
// Animation configuration
// MARK: -
///
private let animation = Animation.spring(
response: 0.8,
dampingFraction: 0.6,
blendDuration: 0.8
response: 0.6, //
dampingFraction: 0.9, //
blendDuration: 0.8 //
)
// MARK: -
var body: some View {
VStack(spacing: 0) {
// Custom navigation bar
//
HStack {
//
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
isPresented = false
}
}) {
HStack(spacing: 4) {
Image(systemName: "chevron.left")
.font(.system(size: 17, weight: .semibold))
Text("Back")
}
.foregroundColor(.blue)
.padding()
}
Spacer()
Text("Settings")
.font(.headline)
.padding()
Spacer()
// Invisible view to balance the layout
Color.clear
.frame(width: 44, height: 44)
}
.background(Color(.systemBackground))
// Settings content
List(0..<1) { _ in
// This empty section ensures proper spacing
Section {
EmptyView()
} header: {
EmptyView()
}
// Add an invisible section header to remove extra top padding
Section(header: EmptyView()) {
EmptyView()
}
// Account & Security
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "person.crop.circle")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("Account & Security")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
// Permission Management
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "lock.shield")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("Permission Management")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
// Support & Service
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "questionmark.circle")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("Support & Service")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
// About Us
HStack {
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
Image(systemName: "info.circle")
.font(.system(size: 24))
.foregroundColor(.gray)
Text("About Us")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.gray)
Color.clear
.frame(width: 12, height: 24)
.background(Color(.systemBackground))
}
.listRowBackground(Color(.systemBackground))
}
.listStyle(GroupedListStyle())
.navigationTitle("Setting")
.navigationBarTitleDisplayMode(.inline)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGray6))
.environment(\.horizontalSizeClass, .regular)
.environment(\.defaultMinListRowHeight, 50)
.listRowInsets(EdgeInsets())
.onAppear {
// Remove extra separators below the list
UITableView.appearance().tableFooterView = UIView()
// Remove separator inset
UITableView.appearance().separatorInset = .zero
// Remove extra space at the top of the table view
UITableView.appearance().contentInset = .zero
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
isAppeared = false
}
// Delay the dismiss to allow the animation to complete
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
withAnimation(animation) {
isPresented = false
}
}) {
@ -155,47 +35,121 @@ struct SettingsView: View {
Image(systemName: "chevron.left")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.blue)
Text("Back")
Text("返回")
.font(.system(size: 17, weight: .regular))
.foregroundColor(.blue)
}
}
Spacer()
//
Text("设置")
.font(.headline)
Spacer()
//
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(.systemBackground))
//
List {
// Section
Section { EmptyView() }
//
settingRow(
icon: "person.crop.circle",
title: "账号与安全",
action: {}
)
//
settingRow(
icon: "lock.shield",
title: "权限管理",
action: {}
)
//
settingRow(
icon: "questionmark.circle",
title: "支持与服务",
action: {}
)
//
settingRow(
icon: "info.circle",
title: "关于我们",
action: {}
)
}
.animation(animation, value: isAppeared)
.listStyle(GroupedListStyle())
.listRowInsets(EdgeInsets())
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
.environment(\.horizontalSizeClass, .regular)
.environment(\.defaultMinListRowHeight, 50)
.onAppear(perform: configureTableView)
}
}
// MARK: -
/// TableView
private func configureTableView() {
// 线
UITableView.appearance().tableFooterView = UIView()
// 线
UITableView.appearance().separatorInset = .zero
//
UITableView.appearance().contentInset = .zero
}
///
/// - Parameters:
/// - icon:
/// - title:
/// - action:
/// - Returns:
private func settingRow(icon: String, title: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack {
//
Image(systemName: icon)
.font(.system(size: 24))
.foregroundColor(.gray)
.frame(width: 40)
//
Text(title)
.foregroundColor(.primary)
Spacer()
//
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.gray)
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
.background(Color(.systemBackground))
}
.buttonStyle(PlainButtonStyle())
.listRowBackground(Color(.systemBackground))
}
}
// MARK: - Preview
// MARK: -
#Preview {
NavigationView {
SettingsView(isPresented: .constant(true))
}
}
// MARK: - Subviews
struct AccountSecurityView: View {
var body: some View {
Text("Account & Security")
}
}
struct PermissionManagementView: View {
var body: some View {
Text("Permission Management")
}
}
struct SupportServiceView: View {
var body: some View {
Text("Support & Service")
}
}
struct AboutUsView: View {
var body: some View {
Text("About Us")
}
}