Although this implementation fulfills all of the requirements outlined in the challenge (see below), the primary focus was on testability and functionality, rather than UI aesthetics. As a result, while features like accessibility and UI polish could be further refined, all functional requirements have been met, and the test coverage exceeds 94%.
Implemented features are marked with an x.
- As a user, I should be able to see a time-ordered list of races ordered by advertised start ascending.
- As a user, I should not see races that are one minute past the advertised start.
- As a user, I should be able to filter my list of races by the following categories: Horse, Harness & Greyhound racing.
- As a user, I can deselect all filters to show the next 5 of all racing categories.
- As a user, I should see the meeting name, race number and advertised start as a countdown for each race.
- As a user, I should always see 5 races and data should automatically refresh.
Tech requirements met are marked with an x.
- Use the latest stable Xcode and a deployment target of the latest stable iOS
- Use Swift 6 Language Mode
- Use SwiftUI and SwiftUI App life cycle, avoid UIKit
- Use Structured Concurrency (e.g. async/await), avoid Combine
- Use Swift Testing not XCTest where possible. Full coverage is not necessary, but there should be at least some testing for key areas/files.
- Consider the user experience with accessibility features such as:
- Dynamic type, e.g. use scalable/adaptive layouts
- VoiceOver and Voice Control, e.g. accessibility elements/labels
- Use custom Decodable implementations where beneficial
- Use SF Symbols for icons, you may import custom symbols
- Documentation e.g. outline requirements you didn’t have time to implement, technical architecture, decisions etc.
Achieving this level of coverage was made possible through advanced techniques such as custom clocks and dates, allowing precise control over the concept of "now".
Testing logic that depends on real-time behavior presents a unique challenge. Constructs like Date.now and Timer introduce non-deterministic behavior because they continuously change their internal state. This leads to two key issues:
-
Unstable Test Results
- Since
Date.nowreturns a different value each time it's accessed, tests relying on it would produce inconsistent results.
- Since
-
Long Test Execution Times
- Testing auto-refresh functionality using
Timerwould require waiting for real-world time intervals, significantly slowing down test runs (e.g., waiting minutes for updates).
- Testing auto-refresh functionality using
With the introduction of Clock in iOS 16, we now have precise control over time-dependent behavior. By using a custom clock, we can:
- Freeze and manipulate time during tests.
- Simulate future or past events instantly.
- Ensure predictable and fast test execution.
This approach enables us to test state changes over time deterministically, making the codebase more reliable and maintainable.
await viewModel.startFetching()
// We're now able to decide how much "time" can "pass" at each step of the test
await advanceTimeBy(seconds: 1, dateProvider: mockDateProvider, clock: testClock)
await #expect(viewModel.races.count == 5)
await advanceTimeBy(seconds: 30, dateProvider: mockDateProvider, clock: testClock)
await #expect(viewModel.races.count == 5)To enforce proper usage of the custom clock, a custom SwiftLint rule called disallow_date_usage has been created. This rule aims to prevent accidental instantiations of Date via direct calls to:
Date()Date.init()Date.now
Unfortunately, since the rule is RegEx-based, it cannot catch all occurrences. For example, it cannot detect cases where .init() is implicitly resolving to Date.
A more robust approach would be to use SwiftSyntax to analyze the Abstract Syntax Tree (AST), which would allow precise detection of all Date instantiations. However, that's beyond the scope of this exercise.
The app follows a modular, scalable architecture, ensuring testability, maintainability, and flexibility (CLEAN principles). The MVVM pattern was used to cleanly(😎) separate concerns:
- Model: Represents race data and business logic.
- ViewModel: Manages state and transforms model data for the UI.
- View: SwiftUI components that display the data.
This approach enhances testability, as the ViewModel can be unit tested independently of SwiftUI (as can be see in RaceSummaryTests and RaceViewModelTests).
The UI leverages Dynamic Type by not only allowing the text to size grow or shrink as necessary, but also by rearranging the different elements on each row. This can be observed in RaceRowView, where the category image is positioned differently depending on available space:
if sizeCategory.isAccessibilityCategory {
categoryImage
}The use of this technology provides a much richer user experience by providing relevant information when needed:
- Each row in the list provides information of itself as a whole, instead of having to cycle through each indivitual element within it.
- Since each row has a countdown element, which automatically updates every second, special meassures have been put in place to avoid overwhelming the user with announcements:
- If the race' start time is 30+ seconds away or if it has already started, the VoiceOver announcement occurs only once
- But if the race' start time is within 30 seconds, the row is treated as having "frequent updates", so the announcements will happen regularly.
.accessibilityElement(children: .ignore)
.conditionalUpdatesFrequentlyTrait(hasFrequentUpdates)
.accessibilityLabel(Text("\(race.meetingName). \(race.category.description). Race \(race.raceNumber). \(hasFrequentUpdates ? "" : accessibilityCountdown)"))
.conditionalAccessibilityValue(hasFrequentUpdates, value: "\(accessibilityCountdown)")Although every file plays an important role within the codebase, there are certain files which showcase the coding prowess better than others. As a suggestion, here's the most important files to review:
RaceViewModel: Here's where most of the heavy-lifting happens, including the auto-refresh logic, which leverages theClockfunctionality.RaceSummary: A model which contains the business logic to check if a minute from the advertised start has passed for a given race. This logic leverages the "custom date" functionality in the form ofDateProvider.RaceViewModelTests: These tests showcase the benefits of usingClockandDateProviderand the Dependency Injection pattern, by testing a variety of complex scenarios.RaceRowView: Demonstrates the use of Dyanmic Type and VoiceOver
