Skip to main content

Dynamische Visualisierung von Benutzeraktivitäten

Beschreibung

Diese View stellt Benutzeraktivitäten anschaulich als farbige Liniendiagramme dar und zeigt, wie sich das Nutzungsverhalten über die Zeit entwickelt. Die Visualisierung aktualisiert sich in Echtzeit während der Interaktion und gibt sofortiges Feedback über die Nutzungstrends der App.

🔍 Zweck

  • Analyse täglicher App-Nutzung für Selbstmanagement oder Digital Wellbeing
  • Gamification durch Visualisieren von Fortschritt oder Engagement-Zielen
  • Nachvollziehen des eigenen Lernverhaltens in Bildungsanwendungen
  • Überwachung und Optimierung von Arbeitsroutinen im Teamkontext
  • Motivation zur Steigerung der Aktivität durch direkte visuelle Rückmeldung

🖥️ Betriebssystem

iOS

📄 Codebeispiel

import SwiftUI

struct ActivityPoint : Identifiable {
    let id = UUID()
    let hourLabel:String   // e.g., "08h"
    var usageMinutes:Int   // e.g., Minutes used within the hour
    
    static func random(hour:String)->ActivityPoint{
        ActivityPoint(hourLabel: hour, usageMinutes:Int.random(in:3...55))
    }
}

struct DynamicActivityGraphView : View {

    @State private var points:[ActivityPoint] =
      (8..<22).map{ h in ActivityPoint(hourLabel:String(format:"%02dh",h), usageMinutes:Int.random(in:5...45)) }

    // Simulate real-time updates when the user taps or interacts.
    func randomizeActivity(){
        withAnimation{
          for i in points.indices{
              points[i].usageMinutes =
                  min(60,max(1,
                  points[i].usageMinutes + Int.random(in:-7...10)))
          }
      }
   }

   var maxY:Int{ max(points.map(\.usageMinutes).max() ?? 60 ,60)}

   var body : some View{
       VStack(alignment:.leading){
           HStack{
               Text("Aktivitätsverlauf heute")
                   .font(.headline)

               Spacer()

               Button(role:.none){
                   randomizeActivity()
               } label:{
                   Label("Neue Aktivität simulieren",
                         systemImage:"waveform.path.ecg.rectangle")
                       .labelStyle(.iconOnly)
                       .imageScale(.large)
               }.accessibilityLabel("Neue Aktivitäten simulieren")
           }.padding([.horizontal,.top])

           GeometryReader{ geo in
               ZStack{
                   // Axes & gridlines:
                   ForEach(Array(stride(from: maxY, to: -1, by: -10)), id: \.self) { yTick in
                       Path { p in
                           let y = yPos(for: yTick, maxY: maxY, hHgt: pxHeight(inGeoSize: .zero))
                           p.move(to: CGPoint(x: 0, y: y))
                           p.addLine(to: CGPoint(x: pxWidth(inGeoSize: .zero), y: y))
                       }
                       .stroke(Color.secondary.opacity(0.12), style:
                           StrokeStyle(lineWidth: (yTick == 0 ? 2 : 1), dash: [4])
                       )
                   }

                   // The animated line graph:
                   Path{ path in
                       guard !points.isEmpty else{return}
                       for (i,p) in points.enumerated(){
                           let x =
                             CGFloat(i)/(CGFloat(points.count - 1))
                                 * geo.size.width
                           let y =
                             yPos(for:p.usageMinutes,maxY:maxY,hHgt:pxHeight(inGeoSize:CGRect(
                                 origin:.zero,size: CGSize(width: 0, height: 0))))
                           if i==0{
                               path.move(to:.init(x:x,y:y))
                           }else{
                               path.addLine(to:.init(x:x,y:y))
                           }
                       }
                   }.stroke(
                     LinearGradient(colors:[.blue,.green,.mint,.yellow],
                                   startPoint:.leading,
                                   endPoint:.trailing),
                     style:
                        StrokeStyle(lineWidth:4,lineCap:.round,lineJoin:.round))

                   // Dots & tooltips:
                   ForEach(Array(points.enumerated()),id:\.1.id){ (idx,pnt) in
                       let x =
                         CGFloat(idx)/(CGFloat(points.count - 1))
                             * geo.size.width
                       let y =
                         yPos(for:pnt.usageMinutes,maxY:maxY,hHgt:pxHeight(inGeoSize:CGRect(
                             origin:.zero,size: CGSize(width: 0, height: 0))))
                       Circle().fill(Color.accentColor.opacity(0.92))
                           .frame(width:10,height:10)
                           .position(x:x,y:y)

                       VStack(spacing:-2){
                           Text("\(pnt.usageMinutes)m")
                               .font(.caption2.weight(.bold))
                               .foregroundColor(.primary.opacity(0.7))
                           Rectangle().frame(width:1,height:(idx%2==0 ?8 :14)).foregroundColor(.gray.opacity(0.28))
                       }.position(x:x,y:y - 22)

                   }

               }

           }.frame(height:210)

           HStack(spacing:-4){
               ForEach(points){pt in
                 Text(pt.hourLabel).font(.caption2).frame(width:nil).minimumScaleFactor(0.6).foregroundColor(.secondary.opacity(0.74)).frame(maxWidth:.infinity,minHeight:nil,maxHeight:nil,alignment:.leading)

               }.padding([.horizontal,.bottom],2)

           }.padding([.horizontal,.bottom],6)

           Divider().padding([.top,.horizontal])

           Text("Tippe auf das ECG-Symbol oben rechts,\num deine Aktivität zu simulieren.")
             .font(.footnote).foregroundColor(.secondary).multilineTextAlignment(.leading)

       }.background(Color(uiColor:.systemBackground))
         .clipShape(RoundedRectangle(cornerRadius:20))
         .shadow(color :.black.opacity(0.05), radius :5,x :0 ,y :3 )
         .padding()
   }

   /// Helper to map a usage value to Y pixel position.
   private func yPos(for val:Int,maxY:Int,hHgt : CGFloat ) -> CGFloat{
       hHgt - CGFloat(val)/CGFloat(maxY)*hHgt + 12 // little padding top/bottom!
   }

   /// Geometry helpers to avoid layout warnings.
   private func pxWidth(inGeoSize rect:CGRect)->CGFloat{320}
   private func pxHeight(inGeoSize rect:CGRect)->CGFloat{170}
}

#Preview{
   DynamicActivityGraphView()
}