Using ChatGPT to Develop a Visual Countdown Timer SwiftUI iOS APP Step by Step
Today, we are going to turn my favorite visual countdown timer to an app.
I have to say, using GPT-4o to develop an app now is way more happier than before. It's much quicker and could remember much longer context. In early GPT-4 version, it might spend 5x more time.
Brief Understanding of Building a iOS app
I used SwiftUI, iOS app and MVP level keywords to make sure that this app would be develop in the way we want.
Sometimes if you don't mention SwiftUI, it might use UIKit which is hard to code to layout. And AI could understand the layout through SwiftUI much more easier so that you could make sure you are talking with the GPT about the same thing.
I might be too aggressive here, because I ask GPT to integrate the apple health. This could be asked later in order to get the MVP app much easier and at higher success rate. But again, GPT-4o is so good and able to process much longer context.
Before getting into the fast tunnel, let me explain how a iOS app works.
The actual coding process to run a app:
Give your app a name
Struct the view, contentview for example
Put the starting view below the @main section (usually at yournameApp.swift)
Write your logic at other swift files (separate logic and view code)
And that is how SwiftUI iOS App Works with MVVM (Model-View-ViewModel)
It's okay that you don't understand. Just remember, the things we are showing is view. And we put the logic code outside of the view because putting all together easy to break.
But you could put all the code in one swift file and that make GPT easy to read but sometimes would out of context.
It's important to let GPT understand the structure and key variable names.
Normally, we would break the development process into 3 steps:
Product Definition
Build the MVP(Minimum Viable Product:) via ChatGPT
Polishing the details via ChatGPT
1. Product Definition
Now, our goal is to make a Visual Countdown Timer App that turns a real visual countdown timer into an app.
It mainly has two features:
A timer that could drag back and forward to adjust remaining time
Save the actual time into Apple Health as medication
2. Build the MVP via ChatGPT
The good thing about developing app with ChatGPT is that you could talk in a very not "coding" way.
For example, you could say
Prompt:
I want to make a countdown timer appBut apparently, it is not specific enough. And the AI won't give you what you exactly want.
If you could directly talk in the coding way, the AI would love you.
Let's enhance the start code
Prompt:
Make a swiftui ios app at mvp level, with these requirements:
1. a countdown app of 15 minutes, it start or pause when it double tap on it
2. when count down stop, record the time and save it as the mediation in apple health
3. use circle slider to flashback to give more time.(the same red cycle at countdown)Now the GPT would give you information how to do it step by step:
> ## 1. Project Setup
> Create a new SwiftUI project in Xcode.
> ## 2. Add HealthKit Capability
> Enable HealthKit in your project:
> 1. Go to your project settings.
> 2. Select the `Signing & Capabilities` tab.
> 3. Click the `+ Capability` button.
> 4. Add `HealthKit`.
> ## 3. Request HealthKit Authorization
> Add a HealthKit manager to handle authorization and data saving.
> ### HealthKitManager.swift
> ## 4. Create the Countdown Timer View
> Add a view for the countdown timer with a circle slider.
> ### ContentView.swift
> ### 5. Circle Slider Implementation
> To give more time, use a circle slider. Add a custom circle slider view.
> ### CircleSlider.swiftAnd then the GPT would print detailed swift file code.
Now, I just write down the swift name here, the code would be shown later.
Create these swift files and click run button to have a try.
But I didn't because I found there are two ContentView. The name of view should not be the same.
You might get this error:
Invalid redeclaration of 'ContentView'Then I asked to fix it.
At the same time, I also let GPT to write the description of info.plist.
The info.plist is important, it is Permission requirements, your app would break or even can't be opened if you didn't write.
Prompt:
There are two struct ContentView?
And please write its info.plist for healthkitAnd then the GPT would try to fix it.
Now the cycle doesn't sync, let's fix it.
Prompt:
the demo works but we need to fix some problems:
1. the timer should link with the cycle, which means as the time decrease, the cycle should decreased at the same time
2. when we touch the cycle and make it more or less, it should also link with the timer. But remember, there should be also another time recording at the same time to record the actual time we spentThen the GPT would update the "TimerViewModel Class" and "ContentView". Or you could update all the code it updated.
Congratulation! Now you have a MVP standard of app.
You could also check your apple health to see if the mediation time got saved.
3. Polishing the details via ChatGPT
There is still some gap between the MVP product to what we really want.
Let't polish it step by step to make these things better:
Change the color and reshape the cycle.
Add the top white knob, double the knob to reset rather than the globe double touch gesutre
Redesign the start and pause button
Prompt:
Nice!
now let's make it much better on ui & ux
1. make the cycle movement more smooth, dont need to wait until next second
2. make the cycle bar in color #FF324A
3. the cycle should totally fill the inside the round shape
4. the full cycle is 25 minutes, there would be a vibration to let you feel a hard stop when you move your finger to adjust every 5 minutes, so there is 6 clock text, 0, 5, 10, 15,20, 25The GPT updated the TimerViewModel.swift, CircleSlider.swift, ContentView.swift and Color Extension.
The color extension looks like this and you could create a new swift file call color.swift.
import SwiftUI
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}That is, if the GPT can't solve the problem, you should rethink about it.
Do we really need that feature?
Now, let's forget the text and try to get the shape right.
I then upload the real timer to GPT-4o.
Then the GPT gave me this.
Prompt:
here is what the app looks like, you should fix these problems:
1. the cycle should go around the center of the round like a clock
2. add a white cycle knob on the top layer and at the center of the cycle that cover the center of the clockThe newest version looks great but feels not right. The problem is when we touch the white knob, it would move the timer which is not intuitive. Let's make the knob touchable and make it double tap to reset the timer to 25 minutes.
Prompt:
1. 25 minutes is the limitation, so when figner move to 25, it stop and don't move over to become 0
2. cancel the double and use a real button to do the job (animate three red dot when countdown and one single red bar when pause)And now we have a minimalist visualize timer app:
It has a smooth synced timer that could move back and forward
Double tap the white knob could reset the timer
Timer start at 15 minutes in default
Let's recap what have we been through:
Ask GPT-4o to create a MVP timer app that has a cycle and timer
Reshape the cycle to be more visually attractive
Polish the design
Remember, you have to setup the info right and add HealthKit in Capability
Copy this to your info (for saving to apple health)
Privacy - Health Share Usage Description: This app uses HealthKit to save your meditation sessions
Privacy - Health Update Usage Description: This app uses HealthKit to save your meditation sessionsAnd here is the structure and code:
Meditation Timer
├───HealthKitManager.swift
├───TimerViewModel.swift
├───CircleSlider.swift
├───Meditation_TimerApp.swift
├───ContentView.swift
└───color.swiftHealthKitManager.swift
import HealthKit
class HealthKitManager {
let healthStore = HKHealthStore()
init() {
requestAuthorization()
}
func requestAuthorization() {
guard HKHealthStore.isHealthDataAvailable() else { return }
let typesToShare: Set = [
HKObjectType.categoryType(forIdentifier: .mindfulSession)!
]
healthStore.requestAuthorization(toShare: typesToShare, read: typesToShare) { success, error in
if !success {
print("HealthKit Authorization Failed: \(String(describing: error?.localizedDescription))")
}
}
}
func saveMeditationSession(startDate: Date, endDate: Date) {
let mindfulType = HKCategoryType.categoryType(forIdentifier: .mindfulSession)!
let mindfulSample = HKCategorySample(type: mindfulType, value: 0, start: startDate, end: endDate)
healthStore.save(mindfulSample) { success, error in
if success {
print("Meditation session saved successfully")
} else {
print("Failed to save session: \(String(describing: error?.localizedDescription))")
}
}
}
}
TimerViewModel.swift
import SwiftUI
import Combine
import CoreHaptics
class TimerViewModel: ObservableObject {
@Published var timeRemaining: TimeInterval = 900 // Start at 15 minutes
@Published var isRunning = false
@Published var progress: Double = 0.6 // Represents 15 minutes out of 25
var startDate: Date?
var adjustedStartDate: Date?
private var timer: AnyCancellable?
private var actualTimeSpent: TimeInterval = 0
private var hapticEngine: CHHapticEngine?
private let healthKitManager = HealthKitManager() // Initialize HealthKitManager
init() {
prepareHaptics()
}
var timeString: String {
let minutes = Int(timeRemaining) / 60
let seconds = Int(timeRemaining) % 60
return String(format: "%02d:%02d", minutes, seconds)
}
func toggleTimer() {
if isRunning {
stopTimer()
} else {
if timeRemaining == 0 {
resetTo25Minutes()
}
startTimer()
}
}
private func startTimer() {
isRunning = true
startDate = Date()
adjustedStartDate = startDate
timer = Timer.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
.sink { _ in
self.timeRemaining -= 0.1
self.progress = self.timeRemaining / 1500
if self.timeRemaining <= 0 {
self.stopTimer()
}
}
}
private func stopTimer() {
isRunning = false
if let startDate = startDate {
actualTimeSpent += Date().timeIntervalSince(startDate)
healthKitManager.saveMeditationSession(startDate: startDate, endDate: Date()) // Save to HealthKit
}
timer?.cancel()
timer = nil
startDate = nil
}
func updateTimerBasedOnProgress() {
timeRemaining = min(1500 * progress, 1500) // Ensure it doesn't go beyond 25 minutes
if timeRemaining == 1500 {
performStrongHapticFeedback()
}
if isRunning {
adjustedStartDate = Date().addingTimeInterval(-actualTimeSpent)
}
}
func resetTo25Minutes() {
timeRemaining = 1500
progress = 1.0
if isRunning {
adjustedStartDate = Date().addingTimeInterval(-actualTimeSpent)
}
performStrongHapticFeedback()
}
private func prepareHaptics() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
do {
hapticEngine = try CHHapticEngine()
try hapticEngine?.start()
} catch {
print("Haptic engine Error: \(error.localizedDescription)")
}
}
private func performStrongHapticFeedback() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
var events = [CHHapticEvent]()
for i in stride(from: 0, to: 1, by: 0.1) {
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(i))
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(i))
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: i, duration: 0.1)
events.append(event)
}
do {
let pattern = try CHHapticPattern(events: events, parameters: [])
let player = try hapticEngine?.makePlayer(with: pattern)
try player?.start(atTime: 0)
} catch {
print("Failed to perform haptic feedback: \(error.localizedDescription)")
}
}
}CircleSlider.swift
import SwiftUI
import CoreHaptics
struct CircleSlider: View {
@Binding var progress: Double
var timerViewModel: TimerViewModel
@State private var engine: CHHapticEngine?
var body: some View {
GeometryReader { geometry in
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.3)
.foregroundColor(Color.gray)
PieChart(progress: min(self.progress, 1.0))
.fill(Color(hex: "#FF324A"))
.animation(.linear)
// Add a white circular knob at the center
Circle()
.fill(Color.white)
.frame(width: 80, height: 80)
.shadow(radius: 4)
.onTapGesture(count: 2) {
self.timerViewModel.resetTo25Minutes()
}
}
.gesture(DragGesture(minimumDistance: 0).onChanged { value in
let radius = min(geometry.size.width, geometry.size.height) / 2
let vector = CGVector(dx: value.location.x - radius, dy: value.location.y - radius)
let angle = atan2(vector.dy, vector.dx) + .pi / 2.0
let fixedAngle = angle < 0.0 ? angle + 2.0 * .pi : angle
self.progress = min(Double(fixedAngle / (2.0 * .pi)), 1.0) // Ensure it doesn't go beyond 1.0 (25 minutes)
self.timerViewModel.updateTimerBasedOnProgress()
// Haptic feedback every 5 minutes
if Int(self.progress * 25) % 5 == 0 {
self.performHapticFeedback()
}
})
.onAppear {
self.prepareHaptics()
}
}
}
private func prepareHaptics() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
do {
self.engine = try CHHapticEngine()
try engine?.start()
} catch {
print("Haptic engine Error: \(error.localizedDescription)")
}
}
private func performHapticFeedback() {
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
var events = [CHHapticEvent]()
for i in stride(from: 0, to: 1, by: 0.1) {
let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(i))
let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(i))
let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: i, duration: 0.1)
events.append(event)
}
do {
let pattern = try CHHapticPattern(events: events, parameters: [])
let player = try engine?.makePlayer(with: pattern)
try player?.start(atTime: 0)
} catch {
print("Failed to perform haptic feedback: \(error.localizedDescription)")
}
}
}
struct PieChart: Shape {
var progress: Double
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
path.move(to: center)
path.addArc(center: center, radius: radius, startAngle: .degrees(-90), endAngle: .degrees(-90 + (progress * 360)), clockwise: false)
path.closeSubpath()
return path
}
}Meditation_TimerApp.swift
import SwiftUI
@main
struct Meditation_TimerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}ContentView.swift
import SwiftUI
struct ContentView: View {
@StateObject private var timerViewModel = TimerViewModel()
private let healthKitManager = HealthKitManager()
var body: some View {
VStack {
ZStack {
CircleSlider(progress: $timerViewModel.progress, timerViewModel: timerViewModel)
.frame(width: 300, height: 300) // Adjust size to fit properly
.padding(40)
}
Text(timerViewModel.timeString)
.font(.largeTitle)
.bold()
.padding(.top, 20)
Button(action: {
timerViewModel.toggleTimer()
if !timerViewModel.isRunning, let startDate = timerViewModel.adjustedStartDate {
healthKitManager.saveMeditationSession(startDate: startDate, endDate: Date())
}
}) {
ZStack {
if timerViewModel.isRunning {
HStack(spacing: 5) {
Circle()
.fill(Color.red)
.frame(width: 10, height: 10)
.offset(y: -5)
.animation(Animation.linear(duration: 0.5).repeatForever(autoreverses: true))
Circle()
.fill(Color.red)
.frame(width: 10, height: 10)
.offset(y: 5)
.animation(Animation.linear(duration: 0.5).delay(0.25).repeatForever(autoreverses: true))
Circle()
.fill(Color.red)
.frame(width: 10, height: 10)
.offset(y: -5)
.animation(Animation.linear(duration: 0.5).delay(0.5).repeatForever(autoreverses: true))
}
} else {
Capsule()
.fill(Color.red)
.frame(width: 30, height: 10)
}
}
}
.padding(.top, 20)
}
}
}
color.swift
import SwiftUI
extension Color {
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}And that's all for today.
This is the first page of Develop with ChatGPT and hope you like it.
Using ChatGPT to build your app is very feasible now. The good thing is you could really have fun while polishing your app rather than just coding.
There are a lot of details we could talk about, but let's wait for another round.













