Hi everyone. I am building my first app store app, a knitting/crochet app.
In my app are projects that are saved to SwiftData. On those project pages are counters, notepad etc & a stopwatch option.
I noticed a bug that when the stopwatch is turned on and is counting fine, if the user is to interact with say the counter, then the stopwatch hangs for a moment then carries on.
I have tried different options with trying to save any data on a background thread, but that just made the app sluggish and un-usable. I am at the point of so much confusion whether its the timer that has an issue, the database or indeed the counter
In my app there is also basic counter that doesn’t save to the database. I added the timer view to check if the same bug happens and it doesn’t. The stopwatch keeps counting whilst the user interacts with the counter no issues which makes me suspect that its something to do with the counter & the stopwatch data being saved.
I have also tried a stopwatch class using Date() instead of Timer but that did the exact same thing…am I going mad or is this a thing with the data?
Here is my Timer
import SwiftUI
import SwiftData
struct TimerView: View {
var projectPart: MultiProjectParts?
var singleProject: SingleProject?
@State var isSingleProject: Bool
@Environment(\.modelContext) private var context
@State private var timeElapsed: TimeInterval = 0
@State private var isRunning = false
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
@State var updatedMultiTimer = projectPart?.timer
@State var updatedSingleTimer = singleProject?.timer
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(.white)
.shadow(radius: 5)
.frame(width: 257, height: 100)
HStack(spacing: 36) {
// Stop
Button {
stop()
} label: {
ZStack {
Circle()
.fill(.sunflower)
.frame(width: 37)
Image(systemName: Constants.timerStop)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30)
.foregroundStyle(.white)
}
}
.padding(.leading, 10)
// Counter
VStack (spacing:10) {
Text(isSingleProject ? timeFormatted(time: updatedSingleTimer!) : timeFormatted(time: updatedMultiTimer!))
.font(.system(size: 24))
.foregroundStyle(.darkGreyText)
.frame(width: 100, height: 50)
}
// Play/pause
Button {
if !isRunning {
start()
}
else if isRunning {
pause()
}
} label: {
ZStack {
Circle()
.fill(.sunflower)
.frame(width: 37)
Image(systemName: isRunning ? Constants.timerPause : Constants.timerPlay)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30)
.foregroundStyle(.white)
}
}
.padding(.trailing, 10)
}
}
.onAppear {
if projectPart?.timer ?? 0.0 > 0 {
// If there is a memory of a timer, load the memory on appear
timeElapsed = projectPart!.timer
}
if singleProject?.timer ?? 0.0 > 0 {
timeElapsed = singleProject!.timer
}
}
.onReceive(timer, perform: { _ in
if self.isRunning {
if !isSingleProject {
self.timeElapsed += 1
// Save to multi model
projectPart?.timer = timeElapsed
try? context.save()
} else {
// Is a single project
self.timeElapsed += 1
singleProject?.timer = timeElapsed
// Save to single model
try? context.save()
}
} else if !self.isRunning && timeElapsed == 0 {
if !isSingleProject {
// Save to multi model
projectPart?.timer = timeElapsed
try? context.save()
} else {
// Is a single project
singleProject?.timer = timeElapsed
try? context.save()
}
}
})
}
// MARK: - Functions
/// Start the timer
private func start() {
isRunning = true
}
/// Pause the timer
private func pause() {
isRunning = false
}
/// Stop & reset the timer
private func stop() {
isRunning = false
timeElapsed = 0
}
/// Format the timer for 00:00:00 string
func timeFormatted(time: TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time.truncatingRemainder(dividingBy: 3600)) / 60
let seconds = Int(time) % 60
return String(format: Constants.timerFormat, hours, minutes, seconds)
}
}```
Here is the counter view
import SwiftUI
struct CounterViewSingle: View {
@Environment(\.modelContext) private var context
@State var isSecondCounter: Bool
@ObservedObject var settings = SettingsData()
var singleProject: SingleProject?
var isBasic: Bool
@State private var counterBasic = 00
@State private var singleCounter1 = 0
@State private var singleCounter2 = 0
@State private var startNumber = 0
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(.white)
.shadow(radius: 5)
.frame(width: 257, height: 100)
HStack(spacing: 36) {
// Minus
Button {
if isBasic {
counterBasic -= settings.counterInterval
}
if !isBasic {
if !isSecondCounter {
singleCounter1 -= settings.counterInterval
} else if isSecondCounter {
singleCounter2 -= settings.counterInterval
}
}
// Save to model
saveCounter(counter1: singleCounter1, counter2: singleCounter2)
} label: {
ZStack {
Circle()
.fill(.sunflower)
.frame(width: 37)
Image(systemName: Constants.counterMinus)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30)
.foregroundStyle(.white)
}
}
// Counter & Reset
VStack (spacing:10) {
if isBasic {
Text("\(counterBasic)")
.font(.system(size: 50))
.foregroundStyle(.darkGreyText)
.frame(width: 95, height: 50)
}
if !isBasic {
if isSecondCounter {
Text("\(singleCounter2)")
.font(.system(size: 50))
.foregroundStyle(.darkGreyText)
.frame(width: 95, height: 50)
} else {
Text("\(singleCounter1)")
.font(.system(size: 50))
.foregroundStyle(.darkGreyText)
.frame(width: 95, height: 50)
}
}
// Reset
Button {
if isBasic {
counterBasic = settings.startNumberOfCounter
}
if !isBasic {
if !isSecondCounter {
singleCounter1 = startNumber
} else if isSecondCounter {
singleCounter2 = startNumber
}
}
// Save to model
saveCounter(counter1: singleCounter1, counter2: singleCounter2)
} label: {
ZStack {
RoundedRectangle(cornerRadius: 15)
.fill(.sunflower)
.frame(width: 90, height: 18)
Text(Constants.counterReset)
.font(.notesText)
.foregroundStyle(.darkGreyText)
}
}
}
// Plus
Button {
if isBasic {
counterBasic += settings.counterInterval
}
if !isBasic {
if !isSecondCounter {
singleCounter1 += settings.counterInterval
} else if isSecondCounter {
singleCounter2 += settings.counterInterval
}
}
// Save to model
saveCounter(counter1: singleCounter1, counter2: singleCounter2)
} label: {
ZStack {
Circle()
.fill(.sunflower)
.frame(width: 37)
Image(systemName: Constants.counterAdd)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30)
.foregroundStyle(.white)
}
}
}
}
.onAppear {
/// Check for data being held for count on projects and check user settings for changes
// Check for a project first
if !isBasic {
// Load any memory of counters
if singleProject?.counter1 ?? 0 > 0 {
singleCounter1 = singleProject!.counter1
}
if singleProject?.counter2 ?? 0 > 0 {
singleCounter2 = singleProject!.counter2
}
// Sort starting number
if settings.startNumberOfCounter != startNumber {
// Check if the original count matched the old start number to then be reset for counter 1
if singleCounter1 == startNumber {
// Set the new baseline number
singleCounter1 = settings.startNumberOfCounter
// Set the new start number
startNumber = settings.startNumberOfCounter
}
// The baseline is already lower than the user has set
else if singleCounter1 < settings.startNumberOfCounter {
// Set the new baseline number
singleCounter1 = settings.startNumberOfCounter
// Set the new start number
startNumber = settings.startNumberOfCounter
}
else {
// Counter already in use, do not reset to new number but change the start number for the next iteration
startNumber = settings.startNumberOfCounter
}
// Check if the original count matched the old start number to then be reset for counter 2
if singleCounter2 == startNumber {
// Set the new baseline number
singleCounter2 = settings.startNumberOfCounter
// Set the new start number
startNumber = settings.startNumberOfCounter
}
// The baseline is already lower than the user has set
else if singleCounter2 < settings.startNumberOfCounter {
// Set the new baseline number
singleCounter2 = settings.startNumberOfCounter
// Set the new start number
startNumber = settings.startNumberOfCounter
}
else {
// Counter already in use, do not reset to new number but change the start number for the next iteration
startNumber = settings.startNumberOfCounter
}
}
} else {
// Change basic counter to new start number
counterBasic = settings.startNumberOfCounter
}
}
}
/// Save counters to the database depending on what counters are being used by user
func saveCounter(counter1: Int, counter2: Int) {
if !isSecondCounter {
singleProject?.counter1 = counter1
} else if isSecondCounter {
singleProject?.counter2 = counter2
}
try? context.save()
}
}
These views are just called in a project view etc so I don’t think the issue is there but if you need any additional code please say. Also the counter view is also using data from App Storage based on user settings such as starting number, count interval etc. Thanks in advance for any advice.