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()
}