Skip to content

Repository files navigation

Next To Go

Overview

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%.

Functional requirements

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

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.

Test Coverage

Test Coverage

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".

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:

  1. Unstable Test Results

    • Since Date.now returns a different value each time it's accessed, tests relying on it would produce inconsistent results.
  2. Long Test Execution Times

    • Testing auto-refresh functionality using Timer would require waiting for real-world time intervals, significantly slowing down test runs (e.g., waiting minutes for updates).

A Better Approach: Controlling "Now" with Custom Clocks

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)

Linting: Preventing Accidental Date Instantiations

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.

Architecture Choices

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).

Accessibility

Dyanmic Type

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
}

VoiceOver

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)")

Most relevant files

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 the Clock functionality.
  • 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 of DateProvider.
  • RaceViewModelTests: These tests showcase the benefits of using Clock and DateProvider and the Dependency Injection pattern, by testing a variety of complex scenarios.
  • RaceRowView: Demonstrates the use of Dyanmic Type and VoiceOver

About

Entain Coding Challenge

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages