Radiale Menüansicht
Diese View zeigt ein zentrales Menü, das beim Antippen radial aufgefächert wird. Um den Hauptbutton gruppieren sich mehrere Menüoptionen kreisförmig, die durch Antippen auswählbar sind. Das Layout bietet eine eindrucksvolle und intuitive Möglichkeit zur Navigation oder zur Auswahl von Aktionen.
🔍 Zweck
- Schnellzugriff auf häufig genutzte Werkzeuge in Zeichen- oder Grafikapps
- Kategorienausswahl in komplexen Apps (z. B. Shopping oder Spiele)
- Kontextmenüs für Textbearbeitung oder Notizen-Apps
- Steuerungselemente für Smart Home-Anwendungen (z. B. Licht, Musik)
- Kamera-App-Modi-Wechsel oder Filterauswahl
🖥️ Betriebssystem
iOS
📄 Codebeispiel
import SwiftUI
// Example usage view with buttons to simulate state changes
struct RadialMenuView: View {
@State private var expanded = false
let menuItems = [
RadialMenuItem(icon:"paintbrush", color:.pink, title:"Brush"),
RadialMenuItem(icon:"scissors", color:.orange, title:"Cut"),
RadialMenuItem(icon:"photo.on.rectangle", color:.yellow, title:"Photo"),
RadialMenuItem(icon:"pencil", color:.green, title:"Draw"),
RadialMenuItem(icon:"trash", color:.red, title:"Delete")
]
// Callback for selection (for demo)
@State private var selectedTitle:String? = nil
var body: some View {
ZStack {
if expanded {
// Semi-transparent background overlay when menu is open.
Color.black.opacity(0.15).ignoresSafeArea().onTapGesture {
withAnimation(.spring()) { expanded.toggle() }
}
}
VStack {
Spacer()
HStack {
ZStack {
// Place each menu item at its radial position.
ForEach(menuItems.indices, id: \.self) { i in
RadialMenuButton(
item: menuItems[i],
angle: Angle.degrees(Double(i) / Double(menuItems.count) * 240 - 185),
distance: expanded ? 105 : 0,
action: {
selectedTitle = menuItems[i].title
withAnimation(.spring()) { expanded.toggle() }
}
)
.opacity(expanded ? 1 : 0)
.scaleEffect(expanded ? 1 : 0.3)
}
Button(action:{
withAnimation(.spring(response: 0.4)) { expanded.toggle() }
}) {
Image(systemName:"plus.circle.fill")
.font(.system(size:56))
.foregroundColor(expanded ? Color.accentColor : Color.primary)
.rotationEffect(.degrees(expanded ? 45 : 0))
.shadow(radius:8)
.padding()
.background(Circle().fill(Color(.systemBackground)).shadow(radius:6))
}
.accessibilityLabel(Text("Menü öffnen/schließen"))
}
Spacer().frame(width:16)
}.padding(.bottom,30)
}
// Demo feedback on selection.
if let title=selectedTitle {
VStack{
Spacer()
Text("\(title) selected")
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal,24).padding(.vertical,12)
.background(Capsule().fill(Color.accentColor.opacity(0.9)))
.transition(.move(edge:.bottom).combined(with:.opacity))
.animation(.easeOut, value:title)
.onAppear{
DispatchQueue.main.asyncAfter(deadline:.now()+1) {
selectedTitle = nil
}
}
Spacer().frame(height:70)
}
}
}
}
}
struct RadialMenuItem {
let icon:String
let color:Color
let title:String
}
struct RadialMenuButton : View {
let item : RadialMenuItem
let angle : Angle // Position of button (degrees from center).
let distance : CGFloat // How far from center.
let action : ()->Void
var body : some View {
Button(action:{ action() }) {
VStack(spacing:-2){
Image(systemName:item.icon)
.font(.system(size:28))
.foregroundColor(item.color)
Text(item.title)
.font(.caption2.bold())
.foregroundColor(.primary.opacity(0.8))
}
.frame(width:60,height:60)
.background(Circle().fill(Color(.systemBackground)))
.overlay(Circle().stroke(item.color,lineWidth:2))
.shadow(color:item.color.opacity(0.2), radius:item.title == "Delete" ? 10 : 5)
}
// Position around central button using polar coordinates.
// The center button is at (0,0); offset by angle/distance.
.offset(
x:(distance * CGFloat(cos(angle.radians))),
y:(distance * CGFloat(sin(angle.radians)))
)
// Accessibility:
.accessibilityLabel(Text(item.title))
}
}
#Preview {
RadialMenuView()
}