Last active
December 30, 2024 20:49
-
-
Save Koshimizu-Takehito/274df8895f607d6a7dea0dfe80c5d907 to your computer and use it in GitHub Desktop.
SwiftUI: Wallet Card Transition
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 | |
struct Card: Identifiable, Hashable { | |
let id = UUID() | |
let creditID = UUID() | |
let colors: [Color] | |
let name: String | |
let number: String | |
} | |
struct Expense: Identifiable, Hashable { | |
let id = UUID() | |
let image: String | |
let name: String | |
let category: String | |
let amount: String | |
let date: String | |
} | |
struct ContentView: View { | |
var cards: [Card] | |
@Namespace var namespace | |
@State var selected: Card? | |
var body: some View { | |
if let selected { | |
DetailContentView(card: selected, namespace: namespace) { | |
withAnimation { | |
self.selected = nil | |
} | |
} | |
} else { | |
ScrollView { | |
LazyVStack(spacing: 12) { | |
ForEach(cards) { data in | |
CardView(card: data) | |
.matchedGeometryEffect(id: data, in: namespace) | |
.onTapGesture { | |
withAnimation { | |
self.selected = data | |
} | |
} | |
} | |
} | |
} | |
.scrollIndicators(.hidden) | |
} | |
} | |
} | |
struct DetailContentView: View { | |
@State var show = false | |
let card: Card | |
let namespace: Namespace.ID | |
let onTap: () -> Void | |
var body: some View { | |
VStack(spacing: 12) { | |
CardView(card: card) | |
.matchedGeometryEffect(id: card, in: namespace) | |
.onTapGesture(perform: onTap) | |
.zIndex(1) | |
ExpenseView() | |
.zIndex(0) | |
} | |
.offset(y: show ? 0 : 400) | |
.onAppear { | |
withAnimation { | |
show = true | |
} | |
} | |
.id(card) | |
} | |
} | |
struct CardView: View { | |
var card: Card | |
var body: some View { | |
HStack { | |
VStack(alignment: .leading, spacing: 26) { | |
Spacer() | |
Spacer() | |
Text(card.name).fontWeight(.semibold) | |
Text(card.number) | |
} | |
.font(.title3) | |
Spacer() | |
} | |
.padding(24) | |
.foregroundStyle(.white) | |
.frame(maxWidth: .infinity) | |
.frame(height: 200) | |
.background( | |
LinearGradient( | |
colors: card.colors, | |
startPoint: .topLeading, | |
endPoint: .bottomTrailing | |
), | |
in: .rect(cornerRadius: 24) | |
) | |
.padding(.horizontal, 20) | |
} | |
} | |
struct ExpenseView: View { | |
private let expenses: [Expense] = .samples | |
@State private var show: Bool = false | |
var body: some View { | |
ScrollView { | |
LazyVStack { | |
ForEach(expenses.indices, id: \.self) { index in | |
let item = expenses[index] | |
HStack(spacing: 16) { | |
Circle() | |
.foregroundStyle(.sampleLinearGradient) | |
.frame(width: 40) | |
VStack(alignment: .leading) { | |
Text(item.name) | |
.font(.headline) | |
Text(item.category) | |
.foregroundStyle(.secondary) | |
} | |
Spacer() | |
VStack(alignment: .trailing) { | |
Text(item.amount) | |
.font(.headline) | |
Text(item.date) | |
.foregroundStyle(.secondary) | |
} | |
} | |
.foregroundStyle(.black) | |
.offset(y: show ? 0 : CGFloat(index * 100)) | |
.opacity(show ? 1 : 0) | |
.animation(.spring(duration: Double(index) * 0.15), value: show) | |
.padding() | |
} | |
} | |
.padding(10) | |
} | |
.scrollIndicators(.hidden) | |
.background(.white, in: .rect(cornerRadius: 24)) | |
.frame(maxHeight: .infinity) | |
.ignoresSafeArea() | |
.padding(.horizontal, 20) | |
.onAppear { | |
withAnimation { | |
show = true | |
} | |
} | |
.onDisappear { | |
withAnimation { | |
show = false | |
} | |
} | |
} | |
} | |
#Preview("ContentView") { | |
ContentView(cards: .samples) | |
} | |
extension ShapeStyle where Self == LinearGradient { | |
static var sampleLinearGradient: Self { | |
let colors: [[Color]] = [ | |
[.orange, .pink], | |
[.mint, .green], | |
[.cyan, .blue], | |
[.purple, .pink], | |
] | |
return LinearGradient( | |
colors: colors.randomElement()!, | |
startPoint: .top, | |
endPoint: .bottom | |
) | |
} | |
} | |
extension [Card] { | |
static let samples: Self = [ | |
Card(colors: [.orange, .pink], name: "TAKEHITO KOSHIMIZU", number: "000 000 000 0001"), | |
Card(colors: [.mint, .green], name: "TAKEHITO KOSHIMIZU", number: "000 000 000 0002"), | |
Card(colors: [.cyan, .blue], name: "TAKEHITO KOSHIMIZU", number: "000 000 000 0003"), | |
Card(colors: [.purple, .pink], name: "TAKEHITO KOSHIMIZU", number: "000 000 000 0004"), | |
] | |
} | |
extension [Expense] { | |
static let samples: Self = [ | |
Expense(image: "amazon", name: "Amazon", category: "Groceries", amount: "$128", date: "20/03/2024"), | |
Expense(image: "dribbble", name: "Dribbble", category: "Membership", amount: "$30", date: "20/03/2024"), | |
Expense(image: "apple", name: "Apple", category: "Apple Pay", amount: "$28", date: "20/03/2024"), | |
Expense(image: "instagram", name: "Instagram", category: "Ad Publish", amount: "$100", date: "20/03/2024"), | |
Expense(image: "netflix", name: "Netflix", category: "Movies", amount: "$55", date: "20/03/2024"), | |
Expense(image: "photoshop", name: "Photoshop", category: "Service", amount: "$348", date: "20/03/2024") | |
] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment