Skip to content

Instantly share code, notes, and snippets.

@Koshimizu-Takehito
Created July 27, 2024 09:36
Show Gist options
  • Save Koshimizu-Takehito/274df8895f607d6a7dea0dfe80c5d907 to your computer and use it in GitHub Desktop.
Save Koshimizu-Takehito/274df8895f607d6a7dea0dfe80c5d907 to your computer and use it in GitHub Desktop.
SwiftUI: Wallet Card Transition
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