Skip to content

Instantly share code, notes, and snippets.

@mireabot
Last active April 7, 2025 07:39
Show Gist options
  • Save mireabot/075a3d44f79ea6d41669885892975e4e to your computer and use it in GitHub Desktop.
Save mireabot/075a3d44f79ea6d41669885892975e4e to your computer and use it in GitHub Desktop.
FinancialHealthSheet
import SwiftUI
struct FinancialHealthSheetPreview: View {
@State private var showFinancialHealthSheet = false
var body: some View {
VStack {
Button(action: {
showFinancialHealthSheet.toggle()
}, label: {
HStack {
Text("Show Financial Health Sheet")
Spacer()
Image(systemName: "arrow.up.right")
}
})
.buttonStyle(BorderedCapsuledButtonStyle())
}
.padding(.horizontal, 16)
.floatingSheet(isPresented: $showFinancialHealthSheet, content: {
FinancialHealthSheet(score: 79)
})
}
}
// MARK: - Extensions
extension FinancialHealthSheetPreview {
struct BorderedCapsuledButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(16)
.background(
Capsule()
.stroke(Color(uiColor: .systemGray5), lineWidth: 1)
)
}
}
}
// MARK: - Previews
#Preview {
FinancialHealthSheetPreview()
}
// MARK: - Financial Health Sheet Base
struct FinancialHealthSheetBase {
var maxDetent: PresentationDetent
var cornerRadius: CGFloat = 20
var interactiveDimiss: Bool = false
var hPadding: CGFloat = 20
var bPadding: CGFloat = 20
}
extension View {
@ViewBuilder
func floatingSheet<Content: View>(isPresented: Binding<Bool>, config: FinancialHealthSheetBase = .init(maxDetent: .fraction(0.99)), @ViewBuilder content: @escaping () -> Content) -> some View {
self
.sheet(isPresented: isPresented) {
content()
.background(Color(uiColor: .systemBackground))
.clipShape(.rect(cornerRadius: config.cornerRadius))
.padding(.horizontal, config.hPadding)
.padding(.bottom, config.bPadding)
.frame(maxHeight: .infinity, alignment: .bottom)
.presentationDetents([config.maxDetent])
.presentationCornerRadius(0)
.presentationBackground(.clear.blendMode(.darken))
.presentationDragIndicator(.hidden)
.interactiveDismissDisabled(config.interactiveDimiss)
}
}
}
// MARK: - Financial Health Sheet View
struct FinancialHealthSheet: View {
let score: Double
@State private var currentScore: Double = 0
let scoreRanges: [ClosedRange<Double>: Color] = [
0...30: .red, // Bad
31...50: .yellow, // Fair
51...80: .blue, // Good
81...100: .green // Great
]
let labels = [
(text: "Bad", color: Color.red),
(text: "Fair", color: Color.yellow),
(text: "Good", color: Color.blue),
(text: "Great", color: Color.green)
]
// Background icons
let icons = ["chart.pie.fill", "person.fill", "plus", "heart.fill", "star.fill", "line.3.horizontal"]
var body: some View {
ZStack(alignment: .top) {
LazyHGrid(rows: Array(repeating: GridItem(.fixed(50)), count: 2), spacing: 20) {
ForEach(0..<12) { index in
Image(systemName: icons[index % icons.count])
.font(.system(size: 24))
.foregroundColor(.gray.opacity(0.7))
.frame(height: 25)
.padding(8)
.background(.gray.opacity(0.15))
.cornerRadius(10)
}
}
.fixedSize(horizontal: true, vertical: true)
.blur(radius: 1.7)
VStack(spacing: 32) {
VStack(spacing: 6) {
Text("Financial health:")
.font(.system(.subheadline, weight: .medium))
VStack(spacing: 3) {
Text("\(Int(score))")
.font(.system(size: 72, weight: .bold))
Text("out of 100")
.font(.title3)
.foregroundColor(.secondary)
}
}
VStack(alignment: .leading, spacing: 10) {
SegmentedProgressBar(score: currentScore, scoreRanges: scoreRanges)
.frame(height: 16)
HStack(spacing: 0) {
ForEach(labels, id: \.text) { label in
Text(label.text)
.font(.caption)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
}
}
.padding(.top, 10)
infoView()
.padding(.bottom, 16)
}
.padding(.top, 20)
.padding(.horizontal, 16)
}
.onAppear {
withAnimation(.spring(duration: 2)) {
currentScore = score
}
}
}
@ViewBuilder
func infoView() -> some View {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 6) {
Text("Why?")
.font(.system(.subheadline, weight: .medium))
.foregroundStyle(.primary)
Text("Savings exceed goals and spending is intentional")
.font(.system(.subheadline, weight: .regular))
.foregroundStyle(.secondary)
.multilineTextAlignment(.leading)
}
.frame(maxWidth: .infinity, alignment: .leading)
Divider().foregroundColor(Color.gray.opacity(0.2)).padding(.vertical, 8)
VStack(alignment: .leading, spacing: 6) {
Text("Recommendation:")
.font(.system(.subheadline, weight: .medium))
.foregroundStyle(.primary)
Text("Stay consistent with your budget, spending and saving habits")
.font(.system(.subheadline, weight: .regular))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 12)
.padding(.vertical, 14)
.overlay {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
}
}
}
// MARK: - FinancialHealthSheet Extenstions
extension FinancialHealthSheet {
// Custom segmented progress bar
struct SegmentedProgressBar: View {
let score: Double
let scoreRanges: [ClosedRange<Double>: Color]
private var indicatorColor: Color {
scoreRanges.first { $0.key.contains(score) }?.value ?? .gray
}
private var segmentColors: [Color] {
scoreRanges.sorted { $0.key.lowerBound < $1.key.lowerBound }.flatMap { range, color in
let count = Int((range.upperBound - range.lowerBound) / 10)
return Array(repeating: color, count: max(1, count))
}
}
var body: some View {
GeometryReader { geometry in
HStack(spacing: 4) {
ForEach(0..<segmentColors.count, id: \.self) { index in
Rectangle()
.fill(segmentColors[index])
.cornerRadius(4, corners: getCornerRadius(for: index))
}
}
.overlay {
// Score indicator
GeometryReader { geo in
Circle()
.fill(indicatorColor)
.stroke(.white, lineWidth: 2)
.frame(width: 32, height: 32)
.position(x: (geo.size.width * score / 100), y: geo.size.height / 2)
}
}
}
}
private func getCornerRadius(for index: Int) -> UIRectCorner {
if index == 0 {
return [.topLeft, .bottomLeft]
} else if index == segmentColors.count - 1 {
return [.topRight, .bottomRight]
}
return []
}
}
}
// MARK: - View Extensions
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape(RoundedCorner(radius: radius, corners: corners))
}
}
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)
}
var animatableData: CGFloat {
get { radius }
set { radius = newValue }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment