Skip to content

Instantly share code, notes, and snippets.

@end3r117
Created June 17, 2020 00:43
Show Gist options
  • Save end3r117/e1aac1401ad375c52e4526fdcdbb87fd to your computer and use it in GitHub Desktop.
Save end3r117/e1aac1401ad375c52e4526fdcdbb87fd to your computer and use it in GitHub Desktop.
SwiftUI - Simple example of a radial menu. Just helping someone on Reddit. Not production code.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
@GestureState private var showOverlay: Bool = false
@GestureState private var longDrag: CGPoint = .zero
@State private var currentButtonColorIndex: Array<Color>.Index = 0
@State private var newButtonColorIndex: Array<Color>.Index? = nil
private var currentButtonColor: Color {
colors[currentButtonColorIndex]
}
private let colors: [Color] = [Color.blue, Color.red, Color.green, Color.yellow]
private static let menuButtonSize: CGSize = .init(width: 100, height: 100)
private let radialMenuSize: CGSize = .init(width: 125, height: 125)
var body: some View {
VStack {
Spacer()
Text("Current Location:")
Text(longDrag == .zero ? " " : String(format: "%.2f", longDrag.x) + ", " + String(format: "%.2f", longDrag.y))
.fixedSize()
.padding(.bottom, 50)
Circle()
.fill(colors[newButtonColorIndex ?? currentButtonColorIndex])
.colorMultiply(showOverlay || longDrag != .zero ? colors[newButtonColorIndex ?? currentButtonColorIndex]: .white)
.frame(width: Self.menuButtonSize.width, height: Self.menuButtonSize.height)
.overlay(
Text("Menu")
.bold()
.animation(nil)
.foregroundColor(.white)
)
.shadow(color: Color(UIColor.black.withAlphaComponent(showOverlay || longDrag != .zero ? 0.8 : 0.3)), radius: showOverlay || longDrag != .zero ? 2 : 4)
.scaleEffect(showOverlay || longDrag != .zero ? 0.95 : 1)
.animation(Animation.spring().speed(4))
.gesture(
LongPressGesture(minimumDuration: 0.1, maximumDistance: .infinity)
.sequenced(before: LongPressGesture(minimumDuration: .infinity))
.updating($showOverlay, body: { (value, state, _) in
switch value {
case .second(true, nil):
state = true
default:
break
}
})
)
.simultaneousGesture(
DragGesture(minimumDistance: 0.1, coordinateSpace: .local)
.updating($longDrag, body: { (value, state, _) in
state = value.location
})
.onChanged({
let dist = $0.location.distance(to: CGPoint(x: Self.menuButtonSize.width / 2, y: Self.menuButtonSize.height / 2))
guard dist >= (Self.menuButtonSize.width / 2) * 0.95 else {
self.newButtonColorIndex = self.currentButtonColorIndex
return
}
self.checkIfTouchEndedOnButton(location: $0.location, finalTouch: false)
})
.onEnded({ (val) in
self.newButtonColorIndex = nil
let dist = val.location.distance(to: CGPoint(x: Self.menuButtonSize.width / 2, y: Self.menuButtonSize.height / 2))
guard dist >= (Self.menuButtonSize.width / 2) * 0.95 else {
return
}
self.checkIfTouchEndedOnButton(location: val.location, finalTouch: true)
})
)
.overlay(
RadialMenuView(isShowing: Binding<Bool>(get: {
self.showOverlay || self.longDrag != .zero
}, set: {_ in }), radialMenuSize: radialMenuSize, colors: colors)
.opacity(showOverlay || longDrag != .zero ? 1 : 0)
.transition(.scale)
.animation(.easeOut(duration: 0.3))
)
Spacer()
}
}
func checkIfTouchEndedOnButton(location: CGPoint, finalTouch: Bool) {
func changeColor(index idx: Int) {
if finalTouch {
if currentButtonColorIndex != idx {
print("FINAL - menu item \(idx) selected with color \(colors[idx]) at location \(location)")
currentButtonColorIndex = idx
}
}else {
if newButtonColorIndex != idx {
print("\(colors[idx]) menu item highlighted at location \(location)")
newButtonColorIndex = idx
}
}
}
let mid = CGPoint(x: Self.menuButtonSize.width / 2, y: Self.menuButtonSize.height / 2)
//Note: this will ONLY work with 4 colors / menu items. Just a demo.
switch location.x {
case mid.x...:
if location.y > mid.y {
changeColor(index: 0)
}else if location.y < mid.y {
changeColor(index: 3)
}
case ..<mid.x:
if location.y > mid.y {
changeColor(index: 1)
}else if location.y < mid.y {
changeColor(index: 2)
}
default:
break
}
}
}
struct RadialMenuView: View {
@Binding var isShowing: Bool
let radialMenuSize: CGSize
let colors: [Color]
var body: some View {
ZStack {
ForEach(colors.indices, id: \.self) { idx in
self.labelForButton(at: idx)
.hueRotation(.degrees(self.isShowing ? 0 : -45))
.rotationEffect(.degrees(self.isShowing ? 0 : 90))
}
}
}
func labelForButton(at idx: Int) -> some View {
Circle()
.trim(
from: isShowing ? (CGFloat(idx + 1) * 0.25) - 0.25 : (CGFloat(idx + 1) * 0.25),
to: CGFloat(idx + 1) * 0.25
)
.stroke(colors[idx], lineWidth: 30)
.frame(width: radialMenuSize.width, height: radialMenuSize.height)
}
}
fileprivate
extension CGPoint {
func distanceSquared(to: CGPoint) -> CGFloat {
CGFloat((self.x - to.x).magnitudeSquared) + CGFloat(((self.y - to.y).magnitudeSquared))
}
func distance(to: CGPoint) -> CGFloat {
sqrt(distanceSquared(to: to))
}
}
PlaygroundPage.current.setLiveView(ContentView())
@end3r117
Copy link
Author

GIF

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment