Radar Scan Animation in SwiftUI
I am currently reworking my Bluetooth scanning app BLE Discover and wanted an animation to convey that scanning was active. I decided to give myself a bit of time and see what I could come up with.

The first thing I wanted to do was to get the basic shape right, so drawing a bordered circle seemed like a good place to start. I opted to use strokeBorder over stroke, as it draws the border inside the shape's bounds instead of over the centre of the shape path. This makes it easier to layout in the view later. No fill color is needed so foregroundStyle is set to clear. For the border color, I added a property to the View so it can be easily changed later.
import SwiftUI
struct RadarScanView: View {
let color: Color
var body: some View {
Circle()
.strokeBorder(color, lineWidth: 2)
.foregroundStyle(.clear)
}
}
#Preview {
RadarScanView(color: .green)
.padding()
}

Now to make the gradient. I want the gradient to follow the shape of a circle, which can be achieved with an AngularGradient. For a basic circle gradient I just need to set the colors and the center. The API also has start and end angles but since this will be moving in a continuous circle they're not needed.
For the colors, I wanted the sweep of the radar to be approximately 1/3rd of the circle so I set three colors: 2 clear and 1 of my chosen color. As the AngularGradient distributes colors evenly, each color gets 120° of the circle and the interpolation between them is handled automatically. To make the radar sweep shorter or longer, I could add or remove clear colors from the array. If I wanted the start of the radar sweep to be less severe I could also change the color's alpha value but for now I like it as is.
I also added a ZStack to encapsulate the Circle and AngularGradient types into one View.
struct RadarScanView: View {
let color: Color
var body: some View {
ZStack {
Circle()
.strokeBorder(color, lineWidth: 2)
.foregroundStyle(.clear)
AngularGradient(
colors: [
.clear,
.clear,
color
],
center: .center
)
}
}
}

This gives a clear view of how the AngularGradient works but it extends out of the circle border. Since this is a simple circle shape, it's easy to fix by clipping the gradient using clipShape.
AngularGradient(
colors: [
.clear,
.clear,
color
],
center: .center,
startAngle: .degrees(0),
endAngle: .degrees(360)
)
.clipShape(.circle)

This is starting to look like what I want, so now I just need to animate it! The animation I want is a continuous 360° rotation, so a natural place to start is the rotationEffect modifier. This rotates a view's rendering from an anchor point by an angle. Center is the default which works here, so I only need to set the angle. I pass 360° or 0° based on a state boolean isRotating. SwiftUI animates changes based on state, so when isRotating changes from false to true, SwiftUI animates the rotation from 0° to 360°.
I then use an animation modifier to set a linear animation that repeats forever. Linear makes sense here as the rotation should be constant speed, not easing in or out. The duration sets the time for one revolution.
Since this animation isn't controlled by user input, I need something to trigger the state change. I set isRotating to true in the .onAppear block, which starts the animation when the view appears.
struct RadarScanView: View {
let color: Color
let animationDuration: TimeInterval
@State var isRotating: Bool = false
var body: some View {
ZStack {
Circle()
.strokeBorder(color, lineWidth: 2)
.foregroundStyle(.clear)
AngularGradient(
colors: [
.clear,
.clear,
color
],
center: .center
)
.clipShape(.circle)
.rotationEffect(.degrees(isRotating ? 360 : 0))
.animation(.linear(duration: animationDuration)
.repeatForever(autoreverses: false),
value: isRotating)
.onAppear {
isRotating = true
}
}
}
}
#Preview {
RadarScanView(
color: .green,
animationDuration: 3
)
}

Done. A simple radar sweep animation with just a few SwiftUI modifiers. I'm happy with how this turned out for BLE Discover and as SwiftUI is so easy to use for this kind of work I can continue playing with it, adding more lines, maybe some dots representing devices found, so much fun to be had! Thanks for reading.