Skip to content

Instantly share code, notes, and snippets.

@mireabot
Last active April 7, 2025 07:39
Show Gist options
  • Select an option

  • Save mireabot/075a3d44f79ea6d41669885892975e4e to your computer and use it in GitHub Desktop.

Select an option

Save mireabot/075a3d44f79ea6d41669885892975e4e to your computer and use it in GitHub Desktop.

Revisions

  1. mireabot revised this gist Mar 9, 2025. 1 changed file with 25 additions and 9 deletions.
    34 changes: 25 additions & 9 deletions FinancialHealthSheet.swift
    Original file line number Diff line number Diff line change
    @@ -75,11 +75,11 @@ struct FinancialHealthSheet: View {
    let score: Double
    @State private var currentScore: Double = 0

    let segmentColors: [Color] = [
    .red, .red, .red, // Bad (0-30)
    .yellow, .yellow, .yellow, // Fair (30-50)
    .blue, .blue, .blue, // Good (50-80)
    .green, .green // Great (80-100)
    let scoreRanges: [ClosedRange<Double>: Color] = [
    0...30: .red, // Bad
    31...50: .yellow, // Fair
    51...80: .blue, // Good
    81...100: .green // Great
    ]

    let labels = [
    @@ -124,7 +124,7 @@ struct FinancialHealthSheet: View {
    }

    VStack(alignment: .leading, spacing: 10) {
    SegmentedProgressBar(score: currentScore, segmentColors: segmentColors)
    SegmentedProgressBar(score: currentScore, scoreRanges: scoreRanges)
    .frame(height: 16)

    HStack(spacing: 0) {
    @@ -191,7 +191,18 @@ extension FinancialHealthSheet {
    // Custom segmented progress bar
    struct SegmentedProgressBar: View {
    let score: Double
    let segmentColors: [Color]
    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
    @@ -206,8 +217,8 @@ extension FinancialHealthSheet {
    // Score indicator
    GeometryReader { geo in
    Circle()
    .fill(.white)
    .stroke(.gray.opacity(0.2), lineWidth: 2)
    .fill(indicatorColor)
    .stroke(.white, lineWidth: 2)
    .frame(width: 32, height: 32)
    .position(x: (geo.size.width * score / 100), y: geo.size.height / 2)
    }
    @@ -245,4 +256,9 @@ struct RoundedCorner: Shape {
    )
    return Path(path.cgPath)
    }

    var animatableData: CGFloat {
    get { radius }
    set { radius = newValue }
    }
    }
  2. mireabot revised this gist Mar 9, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions FinancialHealthSheet.swift
    Original file line number Diff line number Diff line change
    @@ -206,8 +206,8 @@ extension FinancialHealthSheet {
    // Score indicator
    GeometryReader { geo in
    Circle()
    .fill(Color(hex: "#38CA84"))
    .stroke(.white, lineWidth: 2)
    .fill(.white)
    .stroke(.gray.opacity(0.2), lineWidth: 2)
    .frame(width: 32, height: 32)
    .position(x: (geo.size.width * score / 100), y: geo.size.height / 2)
    }
  3. mireabot created this gist Mar 8, 2025.
    248 changes: 248 additions & 0 deletions FinancialHealthSheet.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,248 @@
    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 segmentColors: [Color] = [
    .red, .red, .red, // Bad (0-30)
    .yellow, .yellow, .yellow, // Fair (30-50)
    .blue, .blue, .blue, // Good (50-80)
    .green, .green // Great (80-100)
    ]

    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, segmentColors: segmentColors)
    .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 segmentColors: [Color]

    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(Color(hex: "#38CA84"))
    .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)
    }
    }