Skip to content

Instantly share code, notes, and snippets.

@rgraves-aspiration
Created June 2, 2022 18:19
Show Gist options
  • Save rgraves-aspiration/c821505292e9882454dbae060193cdc6 to your computer and use it in GitHub Desktop.
Save rgraves-aspiration/c821505292e9882454dbae060193cdc6 to your computer and use it in GitHub Desktop.
Experiment with building a Card view in SwiftUi
//
// WalletCard.swift
// Sapling
//
// Created by Ryan Graves on 4/12/22.
//
import SwiftUI
extension String {
func separate(every stride: Int = 4, with separator: Character = " ") -> String {
return String(enumerated().map { $0 > 0 && $0 % stride == 0 ? [separator, $1] : [$1]}.joined())
}
}
func simpleSuccess() {
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
enum CardStatus: CustomStringConvertible {
case active, userLock, pending, blocked
var description: String {
switch self {
// Use Internationalization, as appropriate.
case .active: return "Active"
case .userLock: return "Frozen"
case .pending: return "Pending"
case .blocked: return "Blocked"
}
}
}
enum AspirationCardType: String, CaseIterable {
case spend, plus, zero
}
extension AspirationCardType {
var background: Image {
switch (self) {
case .spend: return Image("BackSpend").resizable()
case .plus: return Image("BackPlus").resizable()
case .zero: return Image("BackZero").resizable()
}
}
var textColor: Color {
switch (self) {
case .spend: return Color(SaplingUIColor.colorSemanticTextDefaultDefault)
case .plus: return Color(SaplingUIColor.colorSemanticTextDefaultInverted)
case .zero: return Color(SaplingUIColor.colorSemanticTextDefaultInverted)
}
}
}
struct WalletCard: View {
@State private var isLoading = false
@State private var showCardDetails: Bool = false
@State private var copied:Bool = false
@State var cardStatus: CardStatus = .active
@State var cardType: AspirationCardType = .spend
var cardholderName: String = "Nikki Santos"
var cardNumberLastFour: String = "4567"
var cardNumber: String = "1234567890123456"
var cardExp: String = "88/88"
var cardCvc: Int = 997
@State private var showViewControls: Bool = false
@State private var maxCardWidth: Double = 384.0
@State private var isEditing = false
var textColor: UIColor = SaplingUIColor.colorSemanticTextDefaultInverted
var body: some View {
ScrollView {
VStack {
VStack {
GeometryReader { metrics in
HStack(spacing: 0) {
Spacer().frame(width: metrics.size.width / 298 * 16)
VStack(alignment: .leading, spacing: 0) {
Spacer().frame(height: metrics.size.height / 188 * 64)
VStack(alignment: .leading, spacing: 0) {
HStack {
Text("Nikki Santos")
.foregroundColor(cardType.textColor)
.textStyle(ButtonOverlineStyle())
Spacer()
if cardStatus != .active {
Text(cardStatus.description)
.foregroundColor(Color(SaplingUIColor.colorSemanticTextDefaultDefault))
.textStyle(ButtonOverlineStyle())
.padding(.top, 3)
.padding(.bottom, 5)
.padding(.horizontal, 8)
.background(Color(SaplingUIColor.colorSemanticBackgroundProgressStrong))
.cornerRadius(4)
}
}.frame(height: 16).offset(y: 4)
Spacer()
HStack(alignment: .lastTextBaseline, spacing: 16) {
Text(showCardDetails
? cardNumber.separate()
: ("••••••••••••" + cardNumberLastFour).separate())
.foregroundColor(cardType.textColor)
.textStyle(BodyLargeStyle())
if(showCardDetails && !copied) {
Text(copied ? "Copied": "Copy")
.foregroundColor(
cardType == .spend
? Color(SaplingUIColor.colorSemanticTextNoticeDefault)
: Color(SaplingUIColor.colorSemanticTextNoticeInverted)
)
.textStyle(BodyStyle())
.onTapGesture {
UIPasteboard.general.setValue(cardNumber, forPasteboardType: "public.plain-text")
copied = true
simpleSuccess()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
// your code here
copied = false
}
}
}
if(copied) {
Text("Copied!")
.foregroundColor(cardType.textColor)
.textStyle(BodyStyle())
.opacity(0.6)
}
}
Spacer()
HStack(spacing: 16) {
VStack(alignment: .leading, spacing: 0) {
HStack {
Text("EXP")
.foregroundColor(cardType.textColor)
.textStyle(ButtonOverlineStyle())
Spacer(minLength: 0)
}
HStack {
Text(showCardDetails ? cardExp : "••/••")
.foregroundColor(cardType.textColor)
.textStyle(BodyStyle())
Spacer(minLength: 0)
}
}
.frame(width: 48)
VStack(alignment: .leading, spacing: 0) {
Text("CVC")
.foregroundColor(cardType.textColor)
.textStyle(ButtonOverlineStyle())
Text(showCardDetails ? String(cardCvc) : "•••")
.foregroundColor(cardType.textColor)
.textStyle(BodyStyle())
}
Spacer()
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Spacer().frame(height: metrics.size.height / 188 * 16)
}
Spacer().frame(width: metrics.size.width / 298 * 16)
}
.background(cardType.background)
}
.aspectRatio(298/188, contentMode: .fit)
.frame(maxWidth: CGFloat(maxCardWidth))
}.frame(height: 250)
VStack(spacing: 0) {
AspirationToggle(isOn: $showCardDetails, label: "Show card details")
AspirationToggle(isOn: $showViewControls, label: "Show diagnostic view controls")
if(showViewControls) {
VStack {
VStack(alignment: .leading, spacing: 4) {
Text("Card Type")
.foregroundColor(Color(SaplingLightDarkColor.colorTextSecondary))
.textStyle(ButtonOverlineStyle())
Picker("Card type", selection: $cardType) {
Text("Spend").textStyle(BodyExtraSmallStyle()).tag(AspirationCardType.spend)
Text("Plus").tag(AspirationCardType.plus)
Text("Zero").tag(AspirationCardType.zero)
}.pickerStyle(.segmented)
}.padding(.vertical)
VStack(alignment: .leading, spacing: 4) {
Text("Card Status")
.foregroundColor(Color(SaplingLightDarkColor.colorTextSecondary))
.textStyle(ButtonOverlineStyle())
Picker("Card Status", selection: $cardStatus) {
Text("Active").tag(CardStatus.active)
Text("Frozen").tag(CardStatus.userLock)
Text("Pending").tag(CardStatus.pending)
Text("Blocked").tag(CardStatus.blocked)
}.pickerStyle(.segmented)
}.padding(.vertical)
VStack(alignment: .leading, spacing: 4) {
Text("Max Width: \(Int(maxCardWidth))")
.foregroundColor(Color(SaplingLightDarkColor.colorTextSecondary))
.textStyle(ButtonOverlineStyle())
Slider(
value: $maxCardWidth,
in: 272...384,
step: 1,
onEditingChanged: { editing in
isEditing = editing
}
).tint(Color.midnight)
Text("The smallest devices the card will appear on will result in a card width of 272. We should restrict it from being wider than 384, and keep it centered in those views.").foregroundColor(Color(SaplingLightDarkColor.colorTextSecondary)).textStyle(BodyExtraSmallStyle())
}.padding(.vertical)
}
}
}
.frame(maxWidth: 384)
}
.padding(.horizontal, 24)
.frame(maxWidth: .infinity)
}
}
}
struct StatefulPreviewWrapper<Value, Content: View>: View {
@State var value: Value
var content: (Binding<Value>) -> Content
var body: some View {
content($value)
}
init(_ value: Value, content: @escaping (Binding<Value>) -> Content) {
self._value = State(wrappedValue: value)
self.content = content
}
}
struct WalletCard_Previews: PreviewProvider {
static var previews: some View {
WalletCard()
.preferredColorScheme(.dark)
// .padding(.horizontal, 24)
// .padding(.vertical, 32)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment