Skip to main content

Interaktive Diagramme

Diese SwiftUI-View zeigt ein interaktives Balkendiagramm, das Verkaufszahlen für verschiedene Produkte visualisiert. Nutzer können auf einzelne Balken tippen, um detaillierte Informationen zu den jeweiligen Verkaufswerten anzuzeigen.

🔍 Zweck

  • Analyse von Verkaufsdaten in einer Vertriebs-App
  • Darstellung von Umsatzzahlen im Management-Dashboard
  • Auswertung von Produktvergleichen im Einzelhandel
  • Visualisierung von Jahresstatistiken in Reporting-Tools
  • Interaktive Präsentation von Ergebnissen in Schulungen oder Workshops

📄 Codebeispiel

import SwiftUI

// Model for sales data
struct ProductSales: Identifiable {
    let id = UUID()
    let productName: String
    let sales: Double
    let color: Color
}

// Main interactive bar chart view
struct InteractiveBarChartView: View {
    let salesData: [ProductSales]
    @State private var selectedProductID: UUID?

    // Calculate max value for scaling bars
    private var maxSales: Double {
        salesData.map { $0.sales }.max() ?? 1
    }

    var body: some View {
        VStack(spacing: 24) {
            Text("Product Sales")
                .font(.title.bold())
                .padding(.top)

            HStack(alignment: .bottom, spacing: 20) {
                ForEach(salesData) { product in
                    BarView(
                        product: product,
                        isSelected: selectedProductID == product.id,
                        maxSales: maxSales
                    )
                    .onTapGesture {
                        withAnimation(.spring()) {
                            if selectedProductID == product.id {
                                selectedProductID = nil // Deselect if tapped again
                            } else {
                                selectedProductID = product.id
                            }
                        }
                    }
                }
            }
            .frame(height: 220)
            .padding(.horizontal, 16)

            // Details overlay for selected bar
            if let selected = salesData.first(where: { $0.id == selectedProductID }) {
                VStack(spacing: 8) {
                    Text(selected.productName)
                        .font(.headline)
                    Text("Sales: \(Int(selected.sales)) units")
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
                .padding()
                .background(RoundedRectangle(cornerRadius: 14).fill(Color(.systemBackground).opacity(0.95)))
                .shadow(radius: 4)
                .transition(.move(edge: .bottom).combined(with: .opacity))
            }

            Spacer()
        }
        .animation(.easeInOut, value: selectedProductID)
        .padding()
        .background(Color(.systemGroupedBackground))
    }
}

// Single bar representation with interactive styling
struct BarView: View {
    let product: ProductSales
    let isSelected: Bool
    let maxSales: Double

    var body: some View {
        VStack {
            ZStack(alignment: .bottom) {
                RoundedRectangle(cornerRadius: 6)
                    .fill(product.color.opacity(isSelected ? 0.7 : 0.35))
                    .frame(width: 38, height: 180)
                
                RoundedRectangle(cornerRadius: isSelected ? 12 : 6)
                    .fill(product.color.opacity(isSelected ? 1.0 : 0.8))
                    .frame(
                        width: isSelected ? 48 : 38,
                        height: CGFloat(product.sales / maxSales) * 170 + (isSelected ? 14 : 0)
                    )
                    .overlay(
                        // Animated sales value label on top of the bar when selected
                        Group {
                            if isSelected {
                                Text("\(Int(product.sales))")
                                    .font(.caption.bold())
                                    .foregroundColor(.white)
                                    .padding(6)
                                    .background(Capsule().fill(product.color.opacity(0.9)))
                                    .offset(y: -44)
                                    .transition(.scale.combined(with: .opacity))
                            }
                        },
                        alignment: .top
                    )
            }
            Text(product.productName)
                .font(.caption2)
                .frame(width: 56)
                .lineLimit(2)
                .multilineTextAlignment(.center)
                .foregroundStyle(isSelected ? product.color : Color.secondary)
        }
        .contentShape(Rectangle()) // Makes the whole area tappable
        .accessibilityElement(children: .combine)
        .accessibilityLabel("\(product.productName), \(Int(product.sales)) units sold")
    }
}

#Preview {
    InteractiveBarChartView(
        salesData: [
            ProductSales(productName: "iPhone", sales: 120, color: Color.indigo),
            ProductSales(productName: "iPad", sales: 80, color: Color.purple),
            ProductSales(productName: "MacBook", sales: 60, color: Color.green),
            ProductSales(productName: "Apple Watch", sales: 45, color: Color.orange),
            ProductSales(productName: "AirPods", sales: 95, color: Color.teal),
        ]
    )
}