Skip to main content

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