-
-
Save VuHD/bd7f3bcfcf09b9dbbf6418f4d3973b11 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| import Charts | |
| // MARK: - Data Model | |
| struct MetricData: Identifiable { | |
| let id = UUID() | |
| let day: String | |
| let value: Double | |
| } | |
| let sampleData = [ | |
| MetricData(day: "Mon", value: 45), | |
| MetricData(day: "Tue", value: 67), | |
| MetricData(day: "Wed", value: 82), | |
| MetricData(day: "Thu", value: 71), | |
| MetricData(day: "Fri", value: 89), | |
| MetricData(day: "Sat", value: 95), | |
| MetricData(day: "Sun", value: 78) | |
| ] | |
| // MARK: - Glass Card Component | |
| struct GlassCard<Content: View>: View { | |
| @Environment(\.colorScheme) var colorScheme | |
| private let content: Content | |
| private let padding: CGFloat | |
| init(padding: CGFloat = 16, @ViewBuilder content: () -> Content) { | |
| self.padding = padding | |
| self.content = content() | |
| } | |
| private var borderGradient: LinearGradient { | |
| if colorScheme == .dark { | |
| return LinearGradient( | |
| stops: [ | |
| .init(color: .white.opacity(0.15), location: 0), | |
| .init(color: .clear, location: 0.4), | |
| .init(color: .black.opacity(0.5), location: 1.0) | |
| ], | |
| startPoint: .top, | |
| endPoint: .bottom | |
| ) | |
| } else { | |
| return LinearGradient( | |
| stops: [ | |
| .init(color: .white.opacity(0.6), location: 0), | |
| .init(color: .clear, location: 0.4), | |
| .init(color: .black.opacity(0.05), location: 1.0) | |
| ], | |
| startPoint: .top, | |
| endPoint: .bottom | |
| ) | |
| } | |
| } | |
| private let cornerRadius: CGFloat = 36 | |
| var body: some View { | |
| content | |
| .padding(padding) | |
| .background( | |
| RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) | |
| .fill(Color(uiColor: .secondarySystemGroupedBackground)) | |
| // ⚠️ PROBLEM: This shadow gets animated during zoom transition | |
| .shadow( | |
| color: .black.opacity(colorScheme == .dark ? 0.3 : 0.05), | |
| radius: 12, | |
| x: 0, | |
| y: 6 | |
| ) | |
| ) | |
| .overlay( | |
| RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) | |
| .stroke(borderGradient, lineWidth: 2) | |
| ) | |
| .contentShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) | |
| } | |
| } | |
| // MARK: - Mini Chart Component | |
| struct MiniMetricChart: View { | |
| let data: [MetricData] | |
| let color: Color | |
| var body: some View { | |
| Chart { | |
| ForEach(data) { item in | |
| LineMark( | |
| x: .value("Day", item.day), | |
| y: .value("Value", item.value) | |
| ) | |
| .interpolationMethod(.catmullRom) | |
| .foregroundStyle(color) | |
| .lineStyle(StrokeStyle(lineWidth: 2.5)) | |
| } | |
| } | |
| .chartXAxis(.hidden) | |
| .chartYAxis(.hidden) | |
| .frame(height: 40) | |
| .padding(.vertical, 8) | |
| } | |
| } | |
| // MARK: - Metric Card Component | |
| struct MetricCard: View { | |
| let title: String | |
| let subtitle: String | |
| let icon: String | |
| let value: String | |
| let unit: String | |
| let color: Color | |
| let data: [MetricData] | |
| var body: some View { | |
| GlassCard(padding: 16) { | |
| VStack(alignment: .leading, spacing: 2) { | |
| // Header | |
| HStack { | |
| Label(title, systemImage: icon) | |
| .font(.system(.headline, design: .rounded, weight: .bold)) | |
| .foregroundStyle(.primary) | |
| Spacer() | |
| Image(systemName: "chevron.right") | |
| .font(.system(size: 14, weight: .semibold)) | |
| .foregroundStyle(.tertiary) | |
| } | |
| Text(subtitle) | |
| .font(.caption) | |
| .foregroundStyle(.secondary) | |
| // Value | |
| HStack(alignment: .firstTextBaseline, spacing: 4) { | |
| Text(value) | |
| .font(.system(.title2, design: .rounded)).bold() | |
| Text(unit) | |
| .font(.system(.callout, design: .rounded, weight: .semibold)) | |
| .foregroundStyle(.secondary) | |
| } | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .padding(.top, 4) | |
| MiniMetricChart(data: data, color: color) | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - Detail View | |
| struct MetricDetailView: View { | |
| let title: String | |
| let data: [MetricData] | |
| let color: Color | |
| var body: some View { | |
| ScrollView { | |
| VStack(alignment: .leading, spacing: 20) { | |
| VStack(alignment: .leading) { | |
| Text("Weekly Average") | |
| .font(.subheadline) | |
| .foregroundStyle(.secondary) | |
| .textCase(.uppercase) | |
| Text(title) | |
| .font(.system(.largeTitle, design: .rounded, weight: .bold)) | |
| } | |
| .padding(.horizontal) | |
| // Large chart | |
| Chart(data) { item in | |
| AreaMark( | |
| x: .value("Day", item.day), | |
| y: .value("Value", item.value) | |
| ) | |
| .foregroundStyle(color.opacity(0.1).gradient) | |
| .interpolationMethod(.catmullRom) | |
| LineMark( | |
| x: .value("Day", item.day), | |
| y: .value("Value", item.value) | |
| ) | |
| .foregroundStyle(color) | |
| .lineStyle(StrokeStyle(lineWidth: 4)) | |
| .interpolationMethod(.catmullRom) | |
| } | |
| .frame(height: 300) | |
| .padding() | |
| } | |
| } | |
| .navigationBarTitleDisplayMode(.inline) | |
| .toolbarBackground(.hidden, for: .navigationBar) | |
| // ⚠️ PROBLEM: Tab bar reappears with ~1 second delay on dismiss | |
| .toolbar(.hidden, for: .tabBar) | |
| } | |
| } | |
| // MARK: - Dashboard Grid View | |
| struct DashboardGridView: View { | |
| let namespace: Namespace.ID | |
| private let columns = [ | |
| GridItem(.flexible(), spacing: 8), | |
| GridItem(.flexible(), spacing: 8) | |
| ] | |
| var body: some View { | |
| LazyVGrid(columns: columns, spacing: 8) { | |
| NavigationLink(value: "tasks") { | |
| MetricCard( | |
| title: "Tasks", | |
| subtitle: "Last 7 days", | |
| icon: "checkmark.circle", | |
| value: "42", | |
| unit: "done", | |
| color: .blue, | |
| data: sampleData | |
| ) | |
| // ⚠️ Using matchedTransitionSource for zoom animation | |
| .matchedTransitionSource(id: "tasks-card", in: namespace) | |
| } | |
| .buttonStyle(.plain) | |
| NavigationLink(value: "steps") { | |
| MetricCard( | |
| title: "Steps", | |
| subtitle: "Last 7 days", | |
| icon: "figure.walk", | |
| value: "8,453", | |
| unit: "avg", | |
| color: .green, | |
| data: sampleData.map { MetricData(day: $0.day, value: $0.value * 100) } | |
| ) | |
| .matchedTransitionSource(id: "steps-card", in: namespace) | |
| } | |
| .buttonStyle(.plain) | |
| } | |
| .padding(.horizontal, 16) | |
| } | |
| } | |
| // MARK: - Main Content View | |
| struct ZoomTransitionDemoContent: View { | |
| @Namespace private var namespace | |
| var body: some View { | |
| NavigationStack { | |
| ScrollView { | |
| VStack(spacing: 20) { | |
| Text("Zoom Transition Issue Demo") | |
| .font(.title2.bold()) | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .padding(.horizontal, 16) | |
| .padding(.top, 20) | |
| Text("Tap a card and watch the shadow + tab bar when you go back") | |
| .font(.caption) | |
| .foregroundStyle(.secondary) | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .padding(.horizontal, 16) | |
| DashboardGridView(namespace: namespace) | |
| } | |
| .padding(.bottom, 100) | |
| } | |
| .background(Color(uiColor: .systemGroupedBackground)) | |
| .navigationDestination(for: String.self) { selection in | |
| if selection == "tasks" { | |
| MetricDetailView( | |
| title: "Tasks Completed", | |
| data: sampleData, | |
| color: .blue | |
| ) | |
| // ⚠️ Using zoom transition - this is where the problem occurs | |
| .navigationTransition( | |
| .zoom(sourceID: "tasks-card", in: namespace) | |
| ) | |
| } else if selection == "steps" { | |
| MetricDetailView( | |
| title: "Steps Walked", | |
| data: sampleData.map { MetricData(day: $0.day, value: $0.value * 100) }, | |
| color: .green | |
| ) | |
| .navigationTransition( | |
| .zoom(sourceID: "steps-card", in: namespace) | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - Tab View Wrapper | |
| struct ZoomTransitionDemoApp: View { | |
| var body: some View { | |
| TabView { | |
| ZoomTransitionDemoContent() | |
| .tabItem { | |
| Label("Home", systemImage: "house") | |
| } | |
| Text("Tab 2") | |
| .tabItem { | |
| Label("Other", systemImage: "star") | |
| } | |
| Text("Tab 3") | |
| .tabItem { | |
| Label("Settings", systemImage: "gear") | |
| } | |
| } | |
| } | |
| } | |
| // MARK: - Preview | |
| #Preview { | |
| ZoomTransitionDemoApp() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment