Instantly share code, notes, and snippets.
Created
June 17, 2020 00:43
-
Star
1
(1)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
GIF