In my previous post, I walked you through how I set up Live Activities in FocusPasta. This seemingly simple feature eventually took me 12 hours to get right, due to a series of unfortunate events.
As promised, here’s why it took so long:
1. Exploring Different Designs
When it comes to app development, UI/UX is often the most challenging part for me — it’s always daunting to design a new feature. For the Live Activity widget in FocusPasta, I wanted to achieve a functional design that still looks good on the lock screen.
Arguably, the design of this widget is even more important than the session view in the main app, as users may spend more time on this screen. It is especially crucial that this widget look polished and functional.
Key Considerations
Essential Information at a Glance
The widget's purpose is to convey essential information quickly and clearly. This means I had to distill the most relevant details into a glanceable, easy-to-read format, ensuring the widget is informative and avoids clutter.
Aesthetic Balance
While functionality was vital, the widget must also be visually balanced and attractive. I could have settled for just two labels — the category name and remaining time — but that wouldn’t have looked very nice.
Consistency
The initial design featured the FocusPasta logo at the top of the widget. I decided to remove it to maintain a more minimalistic look, but the design needs to be unmistakably associated with FocusPasta without explicitly declaring it.
Achieving this involved carefully selecting key elements from the app’s design to include in the widget, ensuring it was instantly recognizable to users familiar with the app. I experimented with various designs, including using different colours for the pasta pot and the progress bar based on the selected category:
I like how these look, but I eventually decided to stick to yellow for all categories. This maintains consistency with the main app and makes the widget more recognizable.
A Bad Habit: Designing on Xcode
In a quest to make my own life more difficult, I have developed a habit of designing directly in Xcode instead of using dedicated tools like Figma or even Canva.
This means that testing each design variation involved building the app, running it on my phone, and starting a focus session—a process that took about three minutes per iteration instead of three seconds with a proper design tool.
Xcode actually has a neat feature called Previews, which lets you see real-time updates of your SwiftUI views as you modify your code. This allows you to visualize the UI quickly without having to run the app on a simulator or a device.
It could have been beneficial if only my laptop had more than a meagre 8GB of RAM.
2. Light Mode vs Dark Mode
Currently, FocusPasta doesn’t support dark mode. I’ve set the app to default to Light Mode regardless of the user’s system settings, which prevents me from being able to adjust the widget’s appearance to align with the user’s preferences.
I wanted the widget to blend seamlessly with other notifications, but achieving this was impossible without dark mode support.
As a workaround, I invested some time into enabling dark mode for FocusPasta while ensuring that the UI looked the same across light and dark modes until I was ready for the actual implementation.
However, this approach led to inconsistencies: built-in elements like the navigation bar title adapted to dark mode colours, while some text labels did not:
If I had persevered and tracked down all instances of these inconsistencies, I might have been able to resolve them. However, I decided just to accept this temporary limitation for now, as I do have plans to implement dark mode in the future once I settle on the colour scheme.
Until then, the app shall remain the odd one out in the notification center of light mode devices.
3. Yet Another Battle with Background Activities
In my final widget implementation, I used the timerInterval
initializer, which allows the widget to manage its timer independently of the main app. This is crucial because the timer in the main app doesn’t continue running in the background.
Reaching this solution wasn’t straightforward.
Running a Timer in the Background
Initially, I attempted to pass the time elapsed as a content state in the widget attributes, hoping that implementing Live Activities would allow the app to continue running when the phone is locked. Unfortunately, this wasn’t the case — the timer in the widget still paused after a few seconds on the lock screen.
I searched high and low across the web for solutions and found that there was no viable way around this. I even found an article dedicated to creating a timer in Live Activities. After cloning and testing the repo, I found it had the same issue.
I share this not to criticize the article but to highlight the hopelessness of the situation. If a tutorial specially devoted to implementing timers in Live Activities also has the same issue, what chance do I have?
I continued to investigate. I even encountered conspiracy theories suggesting that Apple’s timer app may have access to exclusive capabilities. Real iOS developers from actual software companies reported having similar struggles while implementing widgets for their products. The situation seems bleak.
The Silent Audio Hack
Many apps feature a widget with a running timer, so I knew there must be a way. This led me to resort to the infamous workaround, where apps play silent audio to keep the app running in the background. This method is technically not allowed, but I was desperate.
I played an audible audio file to confirm that the audio was playing. Unfortunately, this approach did not keep the timer running, even with the audio playing in the background.
The Light at the End of the Tunnel
I’m not sure why I didn’t find this solution sooner, but I started to see it everywhere once I discovered it. Maybe I was too fixated with getting the timer to run in the background that my tunnel vision led me to tune out other viable solutions.
Eventually, the breakthrough arrived through a thread buried in the depths of Reddit. Essentially, the solution involves the timerInterval
initializer, which accepts a specified time range to define the interval for the timer and automatically updates the timer text and progress bar. You can read all about the implementation here:
Limitations of this Solution
Using the timerInterval
to count down has one significant drawback: when the timer ends, the text stays at “00:00”, and there’s no apparent way to update it. For FocusPasta, I would love for it to change to something like “Well Done!”.
I find comfort in knowing that I am not the only one baffled by this:
The consensus is that there seem to be two possible ways to do this:
Scheduling a background task to update the widget could be unreliable (I have to admit I have yet to try this, as I am afraid it will turn into another 5-hour deep dive).
Setting up a whole-ass server to send push notifications to the widget since widgets can react to push tokens. This sounds excessive, but many developers resort to this due to the unreliability of background tasks. Unfortunately, it will not work if the phone is offline since these push notifications rely on the network.
For now, I shall be happy with “00:00”.
4. Live Activity Not Loading
One unexpected challenge I encountered was that my Live Activity sometimes wouldn’t load.
This was an oversight on my part. It turns out that there is a maximum resolution limit for assets used in Live Activities, which was very clearly documented here.
If an image exceeds the limit, the Live Activity won’t launch — and unfortunately, there’s no warning or error message to indicate this problem.
I had to slowly comment out various lines of code to identify the source of the problem, which was extremely frustrating and time-consuming. Furthermore, once I figured out that the issue was related to asset size, I had to resize each image to ensure they were within the allowable limits for the widget.
5. Sharing Data Structures Between Targets
Another challenge was sharing data structures between the main app and widget extension targets. Initially, I intended to pass the entire Category
object, which is a Realm object, to the widget. This seemed cleaner than passing individual properties like the category name, pasta colour, and pasta type.
struct FocusSessionAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var categoryName: String
var pastaColor: PastaColors
var pastaType: PastaTypes
}
var startDate: Date
var endDate: Date
var countsDown: Bool //true for timer mode, false for stopwatch mode
}
However, this led to complications because the Realm pod could not be accessed in the extension target, which makes sense. After all, the widget extensions are meant to be lightweight. Furthermore, the Category
objects in Realm not only contain the category dimensions but also references to all the related focus sessions.
6. Adjusting the Progress Bar Height
A simple task like adjusting the size of the progress bar turned out to be more challenging than anticipated. The default progress bar looks like this:
Admittedly, it’s not bad at all. But it didn’t feel quite right — I wanted the thickness to match the thickness of the pasta pot, like this:
A thicker progress bar aligned better with the pot’s thickness and made the UI appear more rounded and playful, which complements FocusPasta’s theme.
I initially thought this change would be straightforward, but setting the height parameter only adjusted the height of the container, adding more vertical padding without increasing the thickness of the progress bar.
After considerable trial and error, I eventually discovered that the trick was to adjust the y
value in scaleEffect
. Please don’t ask me why.
ProgressView(
timerInterval: context.attributes.startDate...context.attributes.endDate,
countsDown: context.attributes.countsDown,
label: { Text("") },
currentValueLabel: { Text("") }
)
.progressViewStyle(LinearProgressViewStyle()) to the progress view
.tint(.pastaYellow)
.frame(height: 20)
.scaleEffect(x: 1, y: 1.5, anchor: .center)
Conclusion
Developing this seemingly straightforward Live Activity led to many unexpected challenges, which I have laid out here for your amusement. I find solace in the hope that it might offer you some entertainment.
Seeing the remaining time for my focus session on my lock screen has been a long-awaited feature, and I’m so happy that it’s finally here! Ironically, after all these workarounds, my brain is exhausted, and I don’t anticipate starting a focus session anytime soon.
For now, my focus will be on lying down.
Honestly I am losing hope on implementing live activities. When I start my live activity, (which has the Dynamic Island display two letters on each side) it just turns into a black long bar at the top of the screen. No images are used just a few letters. Any ideas? Or any sample recent repos that I could look at and see how they did it?
ty for the `scaleEffect` trick, saved me a bunch of time