Building a Speedometer App with SwiftUI: A Step-by-Step Guide

Ashish Langhe
4 min readNov 20, 2023

SwiftUI has revolutionized the way we create user interfaces on Apple platforms. With its declarative syntax and powerful features, SwiftUI allows developers to build elegant and efficient apps with less code. In this article, we’ll explore how to create a Speedometer App using SwiftUI, leveraging the framework’s capabilities to design a visually appealing and interactive user interface.

Introduction

Our Speedometer App will consist of a circular meter with an arrow indicating the speed progress. Users can update the speed by tapping the “Update” button and reset it to zero with the “Reset” button. Let’s break down the implementation step by step.

Setting up the Project

To get started, create a new SwiftUI project and replace the default code in the SpeedometerApp struct with the following:

@main
struct SpeedometerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

This code defines the entry point of our app, pointing to the ContentView struct.

ContentView Structure

Now, let’s define the ContentView struct, which serves as the main container for our Speedometer App.

struct ContentView: View {
var body: some View {
Home()
}
}

The ContentView struct simply embeds the Home view, where the main functionality and UI elements are implemented.

Home View

The Home view is where the core functionality of the Speedometer App resides. It includes a meter, update, and reset buttons, and handles the progress state:

struct Home: View {
@State var progress: CGFloat = 0
let colors = [Color("color1"), Color("color2")]

var body: some View {
VStack {
Meter(progress: self.$progress)
HStack(spacing: 25) {
// Update Button
Button(action: {
if self.progress != 100 {
withAnimation(Animation.default.speed(0.55)) {
self.progress += 10
}
}
}) {
// Button styling
Text("Update")
.padding(.vertical)
.frame(width: (UIScreen.main.bounds.width)/2.5)
}
// Button background
.background(Capsule().stroke(LinearGradient(gradient: .init(colors: self.colors), startPoint: .leading, endPoint: .trailing), lineWidth: 2))
.padding()
// Reset Button
Button(action: {
withAnimation(Animation.default.speed(0.55)) {
self.progress = 0
}
}) {
// Button styling
Text("Reset")
.padding(.vertical)
.frame(width: (UIScreen.main.bounds.width)/2.5)
}
// Button background
.background(Capsule().stroke(LinearGradient(gradient: .init(colors: self.colors), startPoint: .leading, endPoint: .trailing), lineWidth: 2))
.padding()
}
}
}
}

The Home view encapsulates the meter, update, and reset buttons in a vertical stack (VStack). The Meter view is responsible for rendering the circular speedometer.

Meter View

The Meter view defines the appearance of our speedometer, including the circular progress bar and arrow indicator:

struct Meter: View {
@Binding var progress: CGFloat
let colors = [Color("color1"), Color("color2")]

var body: some View {
ZStack {
// Circular progress bar
ZStack {
Circle()
.trim(from: 0, to: 0.5)
.stroke(Color.black.opacity(0.2), lineWidth: 55)
.frame(width: 280, height: 280)
Circle()
.trim(from: 0, to: self.setProgress())
.stroke(AngularGradient(gradient: .init(colors: self.colors), center: .center, angle: .init(degrees: 180)), lineWidth: 55)
.frame(width: 280, height: 280)
}
.rotationEffect(.init(degrees: 180))
// Arrow indicator
ZStack (alignment: .bottom) {
// Arrow stem
self.colors[0]
.frame(width: 2, height: 95)
// Arrow head
Circle()
.fill(self.colors[1])
.frame(width: 15, height: 15)
}
.offset(y: -35)
.rotationEffect(.init(degrees: -90))
.rotationEffect(.init(degrees: self.setArrow()))
}
.padding()
.padding(.bottom, -140)
}
// Calculate progress for the circular bar
func setProgress() -> CGFloat {
let temp = self.progress / 2
return (temp * 0.01)
}
// Calculate rotation for the arrow indicator
func setArrow() -> Double {
let temp = self.progress/100
return (temp * 180)
}
}

The Meter view combines two circular shapes to create the progress bar and uses gradients for a visually appealing effect. The arrow indicator is positioned and rotated based on the current progress.

Previewing the App

Finally, let’s add a preview for the ContentView:

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Now, you can run your Speedometer App and see the circular meter in action!

Conclusion

In this article, we explored how to create a Speedometer App using SwiftUI. Leveraging SwiftUI’s declarative syntax, we built a visually appealing and interactive user interface with a circular progress bar and arrow indicator. Feel free to customize and enhance the app further to suit your preferences or integrate it into a larger project.

You can download the GitHub code for the above Speedometer example.

Happy coding!

--

--

Ashish Langhe

I'm Ashish Langhe, a Senior Software Engineer with over 7+ years of expertise in developing native and cross-platform mobile applications.