In this tutorial, we will create a joystick-like control using SwiftUI. The control consists of a larger circle that can be dragged around the screen at your desire and a smaller circle representing the joystick that can move only in the blue circle region. The smaller circle will not pass the edge of the larger circle if it exceeds its radius and will snap back to the center once released. We will also display the angle and distance of the smaller circle from the center of the larger circle.
Prerequisites
- Basic knowledge of SwiftUI and iOS development.
Getting Started
Create a new SwiftUI project in Xcode and open the ContentView.swift file.
Step 1: Define State and Gesture Variables
We define the necessary state and gesture variables in the ContentView struct. Add the following code snippet inside the ContentView struct:
@State private var location: CGPoint = .zero
@State private var innerCircleLocation: CGPoint = .zero
@GestureState private var fingerLocation: CGPoint? = nil
@GestureState private var startLocation: CGPoint? = nil
- location represents the center position of the larger circle (blue circle).
- innerCircleLocation represents the center position of the smaller circle (green circle).
- fingerLocation tracks the current finger position during the drag gesture.
- startLocation stores the starting location when dragging the larger circle.
Step 2: Define Gesture Modifiers
Next, we define two gesture modifiers: simpleDrag and fingerDrag. These modifiers handle the dragging gestures for the larger and smaller circles, respectively.
Add the following code snippet below the gesture variables:
private let bigCircleRadius: CGFloat = 100 // Adjust the radius of the blue circle
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
// Update the location based on the translation of the gesture
var newLocation = startLocation ?? location
newLocation.x += value.translation.width
newLocation.y += value.translation.height
// Calculate the distance between the center of the blue circle and the new location
let distance = sqrt(pow(newLocation.x - location.x, 2) + pow(newLocation.y - location.y, 2))
// Clamp the new location if it exceeds the radius of the blue circle
if distance > bigCircleRadius {
let angle = atan2(newLocation.y - location.y, newLocation.x - location.x)
newLocation.x = location.x + cos(angle) * bigCircleRadius
newLocation.y = location.y + sin(angle) * bigCircleRadius
}
self.location = newLocation
self.innerCircleLocation = newLocation // Update the green circle location
}
.updating($startLocation) { (value, startLocation, transaction) in
startLocation = startLocation ?? location
}
}
var fingerDrag: some Gesture {
DragGesture()
.onChanged { value in
// Calculate the distance between the finger location and the center of the blue circle
let distance = sqrt(pow(value.location.x - location.x, 2) + pow(value.location.y - location.y, 2))
// Calculate the angle between the center of the blue circle and the finger location
let angle = atan2(value.location.y - location.y, value.location.x - location.x)
// Calculate the maximum allowable distance within the blue circle
let maxDistance = bigCircleRadius
// Clamp the distance within the blue circle
let clampedDistance = min(distance, maxDistance)
// Calculate the new location at the edge of the blue circle
let newX = location.x + cos(angle) * clampedDistance
let newY = location.y + sin(angle) * clampedDistance
innerCircleLocation = CGPoint(x: newX, y: newY)
}
.updating($fingerLocation) { (value, fingerLocation, transaction) in
fingerLocation = value.location
}
.onEnded { value in
// Snap the smaller circle to the center of the larger circle
let center = location
innerCircleLocation = center
}
}
- simpleDrag handles the dragging gesture for the larger circle. It updates the location based on the gesture’s translation and clamps the new location if it exceeds the radius of the blue circle.
- fingerDrag handles the dragging gesture for the smaller circle. It calculates the distance and angle between the finger’s location and the center of the blue circle. It clamps the distance within the blue circle and calculates the new location at the edge of the blue circle. The onEnded closure snaps the smaller circle to the center of the larger circle.
Step 3: Add Circle Views and Text
Now, we can add the circle views and text to display the angle and distance information. Update the body property of the ContentView struct with the following code:
var body: some View {
ZStack {
// Larger circle (blue circle)
Circle()
.foregroundColor(.blue)
.frame(width: bigCircleRadius * 2, height: bigCircleRadius * 2)
.position(location)
.gesture(simpleDrag)
// Smaller circle (green circle)
Circle()
.foregroundColor(.green)
.frame(width: 50, height: 50)
.position(innerCircleLocation)
.gesture(fingerDrag)
// Angle text
Text(angleText)
.font(.title)
.foregroundColor(.white)
.bold()
.padding()
.background(Color.black.opacity(0.7))
.cornerRadius(10)
.position(x: UIScreen.main.bounds.width / 2, y: 50)
}
}
- The larger circle (blue circle) is defined using the Circle() view. We set its position to location and attach the simpleDrag gesture to enable dragging.
- The smaller circle (green circle) is defined similarly, using the innerCircleLocation as its position and the fingerDrag gesture.
- The angle text is displayed using the Text view. We set its content to the angleText computed property, which calculates the angle in degrees. We style the text and position it at the top center of the screen.
Step 4: Calculate the Angle in Degrees
Lastly, we need to add the angleText computed property that calculates the angle of the smaller circle from the center of the larger circle in degrees. Update the ContentView struct with the following code snippet:
var angleText: String {
let angle = atan2(innerCircleLocation.y - location.y, innerCircleLocation.x - location.x)
var degrees = Int(-angle * 180 / .pi)
// Convert the degrees to a positive value
if degrees < 0 {
degrees += 360
}
return "(degrees)°"
}
- The angleText property uses the atan2 function to calculate the angle in radians between the innerCircleLocation and the location. It then converts the angle to degrees and ensures it is in the range of 0 to 360.
Conclusion
You have successfully created a joystick-like control in SwiftUI! The control allows you to drag a smaller circle within the boundaries of a larger circle and displays the angle and distance information. You can further customize the appearance and behavior of the control to fit your specific needs.
Feel free to experiment with the code and enhance the control by adding additional features or animations. Happy coding!
Creating a Joystick Control in SwiftUI was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.