Today I will go through a simple example about how to make objects move with SwiftUI. This example will demonstrate the basic drawing concept using just Circle and Path.
Table of Contents
How to make objects move with SwiftUI: what we will achieve
The idea is very simple. I am going to draw two points, make them move on a white 1024 * 768 canvas. And just for fun and to demonstrate a little more dynamic behavior, when the two points are close enough to each other, I will link them with an edge. When they move away from each other, eventually the link will break.
The results will look like this:
Setting up the Mac OS app project
In this example I will use a Mac OS app for simplicity. In Xcode 12, start a new project. Select “Multiplatform” and “App”. I am going to give the project a name “PointSimulation”.
How to make objects move with SwiftUI: The code
The Point data structure
Let’s dive right into the code. The UI is going to be quite straightforward. We will need a white Rectangle as the canvas, two circles representing the points. But before we start with the interface, to represent the data structure of points, I created a Point class:
class Point: ObservableObject{
@Published private var x:CGFloat = 0.0
@Published private var y:CGFloat = 0.0
private var deltax:CGFloat = 0.0
private var deltay: CGFloat = 0.0
init(x: CGFloat, y: CGFloat, deltax: CGFloat, deltay: CGFloat) {
self.x = x
self.y = y
self.deltax = deltax
self.deltay = deltay
}
func getCGPoint()-> CGPoint {
return CGPoint(x: x, y: y)
}
func getX() ->CGFloat {
return x
}
func getY() -> CGFloat {
return y
}
func move() -> Void {
x = x + deltax
y = y + deltay
}
}
The data structure is self explanatory. The variables x and y will represent the x and y coordinates of the point. Note that I have already added a function move(), and two variables deltax and deltay. The move() function will move the point by deltax and deltay (absolute distance on x and y axis) when it is called.
The UI: static version
We are now ready to make the UI. We can start with the following code:
struct PointSimulation: View {
let p1 = Point(x: 500, y: 500, deltax: 1, deltay: 1)
let p2 = Point(x: 530, y: 500, deltax: 1, deltay: -1)
let width = CGFloat(1080)
let height = CGFloat(760)
let range = CGFloat(100)
var body: some View {
ZStack{
Rectangle()
.frame(width: width, height: height)
.foregroundColor(Color.white)
Circle()
.frame(width: 20, height: 20)
.position(x: p1.getX(), y: p1.getY())
.foregroundColor(.red)
Circle()
.frame(width: 20, height: 20)
.position(x: p2.getX(), y: p2.getY())
.foregroundColor(.yellow)
if(CGPointDistance(from: p1.getCGPoint(), to: p2.getCGPoint()) <= range) {
Path { path in
path.move(to: CGPoint(x: p1.getX(), y: p1.getY()))
path.addLine(to: CGPoint(x:p2.getX(), y: p2.getY()))
}
.stroke(Color.blue)
}
}
}
}
func CGPointDistanceSquared(from: CGPoint, to: CGPoint) -> CGFloat {
return (from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y)
}
func CGPointDistance(from: CGPoint, to: CGPoint) -> CGFloat {
return sqrt(CGPointDistanceSquared(from: from, to: to))
}
We use a ZStack here, since we want to put everything on the same canvas. First we add a white rectangle, which is our canvas. The size of the rectangle is 1080 * 760. Then on this canvas we added two circles, each showing one point. The circles’ size is 20 * 20, and we set their position to the corresponding X and Y coordinates. You can see that I have created two points starting at (500, 500) and (530, 530).
To make the link, I set up a range 100 for the distance. When the distance between the two points is less than or equal to the range, we will see the blue link between the two nodes. We draw the link using Path. The way Path works is very simple. It’s like how you draw a line with a pen. We first set the origin point by path.move, and then we draw a line from this point to the next by path.addLine. So, we start from p1 and draw a line to p2. The path shows up if we add .stroke(), where we also configure the color blue.
The distance is calculated by convenience function CGPointDistance.
Now if we start the app we will see the initial points linked together. This is good.
The UI: animated version
Now, how do we make the points move? I have actually added the move() function in the data structure. So all we need is to call it periodically. We will add a timer to make it happen. We now add the highlighted to the code:
struct PointSimulation: View {
@ObservedObject var p1 = Point(x: 500, y: 500, deltax: 1, deltay: 1)
@ObservedObject var p2 = Point(x: 530, y: 500, deltax: 1, deltay: -1)
let width = CGFloat(1080)
let height = CGFloat(760)
let range = CGFloat(100)
let timer = Timer.publish(
every: 0.1, // Second
tolerance: 0.1, // Gives tolerance so that SwiftUI makes optimization
on: .main, // Main Thread
in: .common // Common Loop
).autoconnect()
var body: some View {
ZStack{
Rectangle()
.frame(width: width, height: height)
//.offset(x:0, y:0)
.foregroundColor(Color.white)
Circle()
.frame(width: 20, height: 20)
.position(x: p1.getX(), y: p1.getY())
.foregroundColor(.red)
Circle()
.frame(width: 20, height: 20)
.position(x: p2.getX(), y: p2.getY())
.foregroundColor(.yellow)
if(CGPointDistance(from: p1.getCGPoint(), to: p2.getCGPoint()) <= range) {
Path { path in
path.move(to: CGPoint(x: p1.getX(), y: p1.getY()))
path.addLine(to: CGPoint(x:p2.getX(), y: p2.getY()))
}
.stroke(Color.blue)
}
}
.onReceive(timer) { (_) in
p1.move()
p2.move()
}
}
}
For the sake of simplicity, we start the animation immediately. So I set up the timer as a member and auto connect. The timer fires every 0.1 second. When the timer fires, the onReceive() will get it which in turn moves our two points.
Now let’s start the app again and see what happens. Voila!