Step Counter
Using WidgetKit, App Intents, and optional HealthKit to understand system surfaces and refresh schedules
Widgets often look simple. They sit quietly on the Home Screen, showing a number, a chart, or a small slice of state at a glance. But once you start building them seriously in iOS 26, that simplicity reveals itself as an illusion. Widgets are not passive views. They are scheduled participants in the system, bound by timelines, refresh budgets, and explicit contracts with the operating system.
Step Counter is a deliberately focused app in this iOS 26 micro-apps series. On the surface, it tracks daily steps and renders that information in a widget. Underneath, it explores how WidgetKit, timelines, and data sourcing interact when an app gives up continuous control and instead works within the system’s rules. The goal here isn’t to compete with Apple’s Fitness app or to build a feature-rich tracker. It’s to understand what it means to build against a system surface rather than a traditional app interface.
Why a Step Counter Widget?
A step counter looks trivial at first. It’s just a number that increases over time. But that apparent simplicity hides a set of constraints that make it a strong vehicle for exploring WidgetKit. Step data changes throughout the day, depends on permissions and device capabilities, and needs to be presented in a way that feels current even when refreshes are limited.
Because of that, a step counter forces you to think about time, cadence, and negotiation with the system. You don’t get to update whenever you want. You have to decide how often updates matter, what happens when data isn’t available, and how the widget should behave when the system declines a refresh. Those questions are exactly what make widgets interesting in iOS 26.
Architecture and System Thinking
Step Counter uses a Model–View (MV) architecture, consistent with the other apps in this series. In this context, MV means that models own data and state, views declare presentation, and application behavior lives in services rather than in intermediary layers. There is no additional abstraction tier managing view state, and no attempt to mirror model data into parallel structures.
This choice isn’t about rejecting MVVM as a pattern. MVVM remains a solid approach for apps with complex UI state, shared workflows, or cross-platform synchronization needs. In a widget-centric app, though, introducing ViewModels often adds coordination overhead without providing much benefit. The widget doesn’t orchestrate state changes. It renders whatever state it’s given at a particular point in time.
By keeping responsibilities clear, the architecture stays lean without becoming fragile. Views remain declarative and disposable, services encapsulate behavior, and the system boundaries are respected rather than abstracted away. Architecture here is treated as an optimization problem, not an ideological stance.
WidgetKit Timelines as System Contracts
Unlike a full app, a widget does not run continuously or respond instantly to events. Instead, it operates on a timeline. You propose when the widget should update, and the system decides whether and when that update actually happens. That relationship is fundamental to understanding WidgetKit.
In Step Counter, the timeline provider is the core of that contract. It constructs entries that represent not just data, but a moment in time:
struct StepEntry: TimelineEntry {
let date: Date
let steps: Int
let goal: Int
}
struct StepProvider: TimelineProvider {
func placeholder(in context: Context) -> StepEntry {
StepEntry(date: Date(), steps: 0, goal: 10_000)
}
func getSnapshot(in context: Context, completion: @escaping (StepEntry) -> Void) {
completion(StepEntry(date: Date(), steps: 2_500, goal: 10_000))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<StepEntry>) -> Void) {
let currentSteps = StepService.shared.fetchSteps()
let entry = StepEntry(date: Date(), steps: currentSteps, goal: 10_000)
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
}
}
This code makes the relationship explicit. The widget suggests an hourly cadence, but it doesn’t assume that cadence will be honored exactly. The system remains in control. Once you internalize that, widget design becomes less about forcing updates and more about choosing reasonable moments to reflect change.
The widget view itself stays intentionally simple:
struct StepWidgetEntryView: View {
var entry: StepEntry
var body: some View {
VStack {
ProgressView(value: Double(entry.steps), total: Double(entry.goal))
Text(”\(entry.steps) / \(entry.goal) steps”)
.font(.caption)
}
.padding()
}
}
The UI doesn’t try to be clever. It trusts the timeline entry and renders it predictably. That simplicity is a feature, not a limitation.
HealthKit and Simulation as Engineering Decisions
Step Counter integrates with HealthKit to retrieve real step data on device, but it also supports a simulated HealthKit provider that returns predefined values for development and simulator testing. This isn’t a workaround. It’s a deliberate engineering decision.
Widgets run in constrained environments, and development workflows shouldn’t depend on live sensor data just to validate UI, timelines, or architecture. By abstracting step retrieval behind a service, the widget remains agnostic to where the data comes from. On a real device, HealthKit provides the data. In the simulator, predefined values stand in for it. The widget doesn’t care either way.
That approach keeps HealthKit from becoming the focus of the app. It’s an input, not the story. The story is how data flows through the system and surfaces in a widget under real constraints.
If you want to explore the full implementation details, including how timelines and data sourcing are structured, you can find the complete code here:
GitHub: https://github.com/initMoin/portfolio/tree/main/StepCounter
Looking Beyond Widgets
While not implemented in this version, Step Counter is designed with Live Activities in mind as a natural extension. A walking session surfaced on the Lock Screen or Dynamic Island would complement the widget rather than replace it. The widget offers periodic snapshots. A Live Activity would offer continuous context.
Mentioning this planned direction isn’t about future-proofing the article. It’s about showing how different system surfaces relate to each other in iOS 26. Widgets, Live Activities, and apps aren’t separate silos. They’re coordinated ways of meeting the user at different moments.
What Step Counter Reveals About iOS 26
Building Step Counter clarifies several realities about modern iOS development. Widgets are first-class system surfaces with their own rules. Timelines are negotiated, not guaranteed. Data access needs to degrade gracefully. And architecture still matters, even in small, focused apps.
Most importantly, working within constraints leads to better decisions than trying to work around them. Restraint accelerates understanding faster than unrestricted experimentation.
Closing Thoughts
Step Counter is compact by design, but it represents a meaningful slice of iOS 26. It shows how to think about system surfaces, scheduled updates, and data flow without over-engineering or theatrics. The lessons here aren’t about fitness tracking. They’re about learning to build software that cooperates with the platform instead of fighting it.
In the next article, the focus shifts again, this time to device-to-device interaction and spatial feedback with Hot-Cold Finder. The surface changes, but the discipline remains the same.

