Lee Johnson - Apple Watch Developer

Enhancing the MetricEdition Apple Watch App: A Comprehensive Guide to Proposed Changes

Innovation often starts with curiosity. MetricEdition, created by Lee Johnson under the StromaScape brand, is a perfect example of how curiosity about time representation can lead to a polished, standalone Apple Watch app built entirely with SwiftUI.

The MetricEdition app is a innovative Apple Watch application designed to display time in the French Revolutionary (metric or decimal) time system, which divides the day into 10 hours, each with 100 minutes and 100 seconds. This system, rooted in historical attempts to decimalize time during the French Revolution, offers a mathematically simpler alternative to the standard 24-hour clock. The app includes a visually engaging clock face with a rotating sun/moon indicator, haptic feedback for hour changes, a night mode with a red theme, and informational tabs about the history of metric time and the app itself.

Below is an honest, technical review of the app’s code design, UI structure, and performance characteristics, followed by constructive improvement suggestions and potential extensions.

Current FULL Working Code


//
//  MetricEditionApp.swift
//  MetricEdition Watch App
//
//  Created by Lee Johnson on 23/05/2023.
//  Updated 15/09/2023
//  Updated to include the sun/moon visual to help with time association.
//  Added haptic feedback for 24hr & metric hours of the day.
//  Added a red nightmode with toggle.
//  Added a description of how metrictime came to be.
//

// Import necessary libraries
import SwiftUI
import WatchKit

// Main view
struct ContentView: View {
// Define necessary state variables
@State private var selectedTab = 1
@State private var fractionOfDay: Double = 0.0
@State private var isHapticsOn = false
@State private var isMetricHapticsOn = false
@State private var isNightModeOn = false
@State private var previousMetricHour: Int?
// Create a timer that fires every second
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

// Main body of the view
var body: some View {
// Create a tab view
TabView(selection: $selectedTab) {
// First tab
ZStack {
// Display sun/moon image and rotate based on time of day
Image("sunmoon")
.resizable()
.scaledToFit()
.rotationEffect(Angle(degrees: -360 * -fractionOfDay), anchor: .center)
.frame(width: 74, height: 74)
.padding(.top, -60)
.colorMultiply(isNightModeOn ? Color(hex: "ea3323") : .white)

// Display background image
Image("stromascape-face")
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.colorMultiply(isNightModeOn ? Color(hex: "ea3323") : .white)

// Display time and date

VStack {
// Display the time in HH:MM:SS format
Text(timeString(from: fractionOfDay))
.font(Font.system(size: 32))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.black)
.frame(width: 120, height: 34)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color(hex: "171717"), lineWidth: 1.5)
.shadow(color: .black, radius: 4, x: 1, y: 1)
)
)
.fontWeight(.bold)
.padding(.top, 45)

HStack {
Spacer()
// Display the day of the week
Text(getDayOfWeek())
.font(.system(size: 15))
Spacer()
// Display the month and day
Text(getMonthAndDay().uppercased())
.font(.system(size: 15))
Spacer()
}
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.background(
RoundedRectangle(cornerRadius: 4)
.fill(Color.black)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color(hex: "171717"), lineWidth: 1.5)
.shadow(color: .black, radius: 4, x: 1, y: 1)
)
)
.fontWeight(.bold)
.padding(15)
.frame(width: 200, height: 40)
}
}
.tag(1)

// Second tab
ScrollView {
VStack(alignment: .center) {
Toggle("Night Mode", isOn: $isNightModeOn)
.font(.system(size: 13))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.padding()


// Display toggle switches for haptic feedback
Toggle("24 Hour Haptic", isOn: $isHapticsOn)
.font(.system(size: 13))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.padding()
.onChange(of: isHapticsOn) { newValue in
if newValue {
WKInterfaceDevice.current().play(.success)
}
}

// Display the toggle switch for metric hour haptic feedback
// Display description for the toggle switches
Toggle("Metric Hour Haptic", isOn: $isMetricHapticsOn)
.font(.system(size: 13))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.padding()
.onChange(of: isMetricHapticsOn) { newValue in
if newValue {
WKInterfaceDevice.current().play(.success)
}
}

// Text description for the toggle switches
Text("Enable toggle(s) to receive haptic feedback and sound on every standard or metric hour.")
.font(.system(size: 13))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.padding()

}
}
.tag(2)

// Third tab
ScrollView {
// Display historical information about metric time
VStack(alignment: .leading, spacing: 40) {
Image("stromascape")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.padding(.top, -35)
.colorMultiply(isNightModeOn ? Color(hex: "ea3323") : .white) // Apply colorMultiply after setting other modifiers

let text = """
In 1793, the French introduced French Revolutionary Time as a replacement for the traditional clock system. This new system consisted of a 10-hour day, with 100 minutes per hour, & 100 seconds per minute. The main advantage of this modern system was its simplicity in time-related calculations. For example, determining when a day is 80% complete could be expressed as "at the end of the eighth hour" in decimal time, whereas standard time would require saying "at 19 hours, 12 minutes.". The adoption of French Revolutionary Time faced challenges due to the deeply ingrained habits of the population & the lack of practical reasons for non-mathematicians to switch to the new system. Clock faces were manufactured to display both decimal time & standard time, leading to confusion. Additionally, the expense of replacing all clocks & watches in the country was a significant barrier. After just 17 months, the French discontinued the use of decimal time, making it non-mandatory starting from April 7, 1795. Despite this, some regions continued to observe decimal time, & a few decimal clocks remained in use for years afterward, resulting in potential missed appointments.
"""
let sentences = text.components(separatedBy: ". ")
ForEach(sentences, id: \.self) { sentence in
Text(sentence + ".")
.font(.system(size: 14))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.scrollTransition(.animated.threshold(.visible(1))) { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0.72)
.scaleEffect(phase.isIdentity ? 1 : 0.35)
.blur(radius: phase.isIdentity ? 0 : 0.1)
}

}

Spacer()
}
.padding(.horizontal, 4)
}
.tag(3)

// Fourth tab
ScrollView {
// Display information about the app and the developer
VStack(alignment: .leading, spacing: 40) {
Image("stromascape")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.padding(.top, -35)
.colorMultiply(isNightModeOn ? Color(hex: "ea3323") : .white) // Apply colorMultiply after setting other modifiers


let text = """
Metric Time (v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "")) is an app that's been created by Lee Johnson. It falls under the StromaScape brand & was built to self-learn SwiftUI and this is my first app. It does not use any external third-party libraries. It runs entirely on your own watch & contains no advertisments & never will. It uses Apple App Store crash analytics for crash reports only. If you require any support, feel free to email me (Lee) at: faces@stromascape.com
"""
let sentences = text.components(separatedBy: ". ")
ForEach(sentences, id: \.self) { sentence in
Text(sentence + ".")
.font(.system(size: 14))
.foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
.scrollTransition(.animated.threshold(.visible(1))) { content, phase in
content
.opacity(phase.isIdentity ? 1 : 0.72)
.scaleEffect(phase.isIdentity ? 1 : 0.35)
.blur(radius: phase.isIdentity ? 0 : 0.1)
}
}
}

Spacer()
}
.padding(.horizontal, 4)
}
.tag(4)

// Set tab view style and frame
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) // Hide the tab bar
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black)
.colorMultiply(isNightModeOn ? Color(hex: "ea3323"): .white) // Apply colorMultiply after setting other modifiers
// Set up actions for when the view appears and when the timer fires
.onAppear {
self.render()
}
.onReceive(timer) { _ in
self.render()
}
}

// Function to calculate time of day and trigger haptic feedback
func render() {
// Calculate time of day
let date = Date()
let timeOfDay = Double(date.hours * 60 * 60 * 1000 +
date.minutes * 60 * 1000 +
date.seconds * 1000 +
date.milliseconds)
fractionOfDay = timeOfDay / 86400000

// Trigger haptic feedback for start of new hour and start of new metric hour

// Check if it's the start of a new hour
if isHapticsOn && date.minutes == 0 && date.seconds == 0 {
// Trigger haptic feedback
WKInterfaceDevice.current().play(.success)
}

// Check if it's the start of a new metric hour
let metricHour = Int(fractionOfDay * 10)
if isMetricHapticsOn && metricHour != previousMetricHour {
// Trigger haptic feedback
WKInterfaceDevice.current().play(.success)
previousMetricHour = metricHour
}
}

// Function to convert time of day to string format
func timeString(from fractionOfDay: Double) -> String {
// Calculate hours, minutes, and seconds
let hh = Int(fractionOfDay * 10)
let mm = Int((fractionOfDay * 10 - Double(hh)) * 100)
let ss = Int((fractionOfDay * 100000).truncatingRemainder(dividingBy: 100))
// Return time string
return "\(hh):\(String(format: "%02d", mm)):\(String(format: "%02d", ss))"
}

// Function to get day of the week
func getDayOfWeek() -> String {
// Format and return date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: Date()).uppercased()
}

// Function to get month and day
func getMonthAndDay() -> String {
// Format and return date
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMM d"
return dateFormatter.string(from: Date())
}
}

// Extension to get hours, minutes, seconds, and milliseconds from a date
extension Date {
var hours: Int { Calendar.current.component(.hour, from: self) }
var minutes: Int { Calendar.current.component(.minute, from: self) }
var seconds: Int { Calendar.current.component(.second, from: self) }
var milliseconds: Int { Calendar.current.component(.nanosecond, from: self) / 1000000 }
}

// Extension to create a color from a hex string
extension Color {
init(hex: String) {
let scanner = Scanner(string: hex)
var rgbValue: UInt64 = 0

scanner.scanHexInt64(&rgbValue)

self.init(
.sRGB,
red: Double((rgbValue & 0xFF0000) >> 16) / 255.0,
green: Double((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: Double(rgbValue & 0x0000FF) / 255.0,
opacity: 1.0
)
}
}

// Preview provider
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

A Comprehensive Guide to Proposed Changes

Introduction

The app code is well-structured for a beginner's project, demonstrating a solid grasp of SwiftUI basics, including state management, timers, and WatchKit integrations for haptics. However, as with any app, there is room for improvement in areas such as code modularity, user experience (UX), performance optimization, accessibility, persistence of user settings, and adherence to modern SwiftUI best practices. These enhancements can make the app more maintainable, efficient, and user-friendly, potentially increasing its appeal and usability on the Apple Watch platform.

Section 1: Code Structure and Modularity

One of the primary areas for improvement is breaking down the monolithic ContentView into smaller, reusable components. The current code packs everything into a single view, which can become unwieldy as the app grows.

Change 1.1: Extract Clock Face into a Separate View

Description of the Change: Create a new SwiftUI view called ClockFaceView that encapsulates the ZStack containing the sun/moon image, background, time display, and date elements from the first tab. Pass necessary states like fractionOfDay and isNightModeOn as bindings or parameters.

Reason for the Change: In SwiftUI, modularity promotes reusability and readability. The current ContentView is over 200 lines long, mixing UI elements from multiple tabs. Extracting the clock face reduces cognitive load, makes debugging easier, and allows for easier testing or reuse in future expansions (e.g., complications). This aligns with Apple's Human Interface Guidelines (HIG) for WatchOS, which emphasize simple, focused views due to the small screen size.

What the Change Does: The ClockFaceView becomes a self-contained component that calculates and displays the metric time, rotates the sun/moon based on fractionOfDay, applies night mode coloring, and shows the day/week. When the parent ContentView updates fractionOfDay, this view re-renders efficiently. This separation means changes to the clock UI won't affect other tabs, improving maintainability. For instance, if you want to add animations to the time text, you can do so isolated in this view without risking side effects elsewhere.

Code Snippet:

Before (in ContentView):

// ... inside TabView
ZStack {
    Image("sunmoon") // ... rest of the clock elements
}
.tag(1)

After:

Create ClockFaceView.swift:

struct ClockFaceView: View {
    var fractionOfDay: Double
    var isNightModeOn: Bool

    var body: some View {
        ZStack {
            Image("sunmoon")
                .resizable()
                .scaledToFit()
                .rotationEffect(Angle(degrees: -360 * -fractionOfDay), anchor: .center)
                .frame(width: 74, height: 74)
                .padding(.top, -60)
                .colorMultiply(isNightModeOn ? Color(hex: "ea3323") : .white)

            // ... rest of the ZStack elements, including timeString, getDayOfWeek, etc.
        }
    }

    // Include timeString, getDayOfWeek, getMonthAndDay functions here or make them shared.
}

In ContentView:

ClockFaceView(fractionOfDay: fractionOfDay, isNightModeOn: isNightModeOn)
    .tag(1)

This extraction alone reduces ContentView by about 50 lines, making it more manageable.

Change 1.2: Extract Settings Tab into a Separate View

Description of the Change: Move the second tab's ScrollView with toggles and description into a new SettingsView struct, passing bindings for isNightModeOn, isHapticsOn, isMetricHapticsOn.

Reason for the Change: Similar to the clock face, this promotes separation of concerns. Settings logic (like haptic toggles) is distinct from display logic. This makes it easier to add more settings in the future, such as customizable colors or sound options, without bloating the main view. WatchOS apps benefit from lightweight views to ensure smooth scrolling and responsiveness on limited hardware.

What the Change Does: SettingsView handles all user interactions for toggles, including the .onChange modifiers that play haptics when enabled. The description text is now localized within this view, allowing for potential dynamic updates (e.g., based on user preferences). When a toggle changes, it triggers the haptic only if newly enabled, and the state propagates back to the parent via bindings. This view can now be previewed independently in Xcode, speeding up development iterations.

Code Snippet:

After creating SettingsView.swift:

struct SettingsView: View {
    @Binding var isNightModeOn: Bool
    @Binding var isHapticsOn: Bool
    @Binding var isMetricHapticsOn: Bool

    var body: some View {
        ScrollView {
            VStack(alignment: .center) {
                Toggle("Night Mode", isOn: $isNightModeOn)
                    .font(.system(size: 13))
                    .foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
                    .padding()

                Toggle("24 Hour Haptic", isOn: $isHapticsOn)
                    .font(.system(size: 13))
                    .foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
                    .padding()
                    .onChange(of: isHapticsOn) { newValue in
                        if newValue {
                            WKInterfaceDevice.current().play(.success)
                        }
                    }

                // ... similarly for Metric Hour Haptic

                Text("Enable toggle(s) to receive haptic feedback and sound on every standard or metric hour.")
                    .font(.system(size: 13))
                    .foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
                    .padding()
            }
        }
    }
}

In ContentView:

SettingsView(isNightModeOn: $isNightModeOn, isHapticsOn: $isHapticsOn, isMetricHapticsOn: $isMetricHapticsOn)
    .tag(2)

This modular approach enhances scalability; for example, adding a new toggle for "Vibrate Intensity" would only modify SettingsView.

Change 1.3: Extract Info Tabs into Reusable TextContentView

Description of the Change: Create a generic TextContentView for the history and about tabs (tabs 3 and 4). This view takes parameters for the title image, text content, and night mode state. Use it for both informational sections.

Reason for the Change: The history and about tabs have duplicated code structures: an image, split text into sentences with scroll transitions, and spacing. Duplication violates DRY (Don't Repeat Yourself) principles, leading to maintenance issues. A reusable view reduces code redundancy and ensures consistent styling across info pages.

What the Change Does: TextContentView renders the top image with color multiply for night mode, splits the provided text into sentences, and applies the scroll transition effect to each. The .scrollTransition modifier animates opacity, scale, and blur as the user scrolls, creating a smooth reveal effect. By parameterizing the text, this view can be reused for any future info tabs, like a "How to Use" section. The ForEach loop efficiently handles dynamic sentence counts, and Spacer ensures content fills the scroll view vertically.

Code Snippet:

Create TextContentView.swift:

struct TextContentView: View {
    var imageName: String
    var text: String
    var isNightModeOn: Bool

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 40) {
                Image(imageName)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 18, height: 18)
                    .padding(.top, -35)
                    .colorMultiply(isNightModeOn ? Color(hex: "ea3323") : .white)

                let sentences = text.components(separatedBy: ". ")
                ForEach(sentences, id: \.self) { sentence in
                    Text(sentence + ".")
                        .font(.system(size: 14))
                        .foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f"))
                        .scrollTransition(.animated.threshold(.visible(1))) { content, phase in
                            content
                                .opacity(phase.isIdentity ? 1 : 0.72)
                                .scaleEffect(phase.isIdentity ? 1 : 0.35)
                                .blur(radius: phase.isIdentity ? 0 : 0.1)
                        }
                }
                Spacer()
            }
            .padding(.horizontal, 4)
        }
    }
}

In ContentView:

TextContentView(imageName: "stromascape", text: """
In 1793, the French introduced... // full history text
""", isNightModeOn: isNightModeOn)
    .tag(3)

TextContentView(imageName: "stromascape", text: """
Metric Time (v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "")) is an app... // full about text
""", isNightModeOn: isNightModeOn)
    .tag(4)

This reuse cuts down on repeated code, making updates (e.g., changing font size) a single-point change.

Section 2: State Management and Persistence

The app uses @State for toggles, but these reset on app relaunch. Persisting them improves UX.

Change 2.1: Use @AppStorage for Persistent Toggles

Description of the Change: Replace @State with @AppStorage for isHapticsOn, isMetricHapticsOn, and isNightModeOn. Add default values.

Reason for the Change: WatchOS users expect settings to persist across sessions. @State is ephemeral; @AppStorage uses UserDefaults, surviving app closures. This is a standard practice in SwiftUI for simple persistence, avoiding heavier solutions like Core Data for this app.

What the Change Does: @AppStorage reads/writes to UserDefaults automatically. On first launch, it uses defaults (e.g., false for haptics). When a toggle changes, it's saved instantly. On relaunch, states restore, so if night mode was on, it remains on. This seamless persistence enhances user satisfaction, as they don't need to reconfigure every time.

Code Snippet:

Before:

@State private var isHapticsOn = false

After:

@AppStorage("isHapticsOn") private var isHapticsOn = false
// Similarly for others

No other changes needed; bindings work the same.

Change 2.2: Initialize previousMetricHour on Appear

Description of the Change: In render(), set previousMetricHour on first call if nil.

Reason for the Change: Currently, previousMetricHour is optional and nil initially, but the first metric hour change might not trigger haptic if not initialized. This ensures consistent behavior from app start.

What the Change Does: On onAppear, render() runs, calculating metricHour and setting previousMetricHour if nil, preventing a missed haptic on the first check. Subsequent timer fires compare against this value.

Code Snippet:

In render():

let metricHour = Int(fractionOfDay * 10)
if previousMetricHour == nil {
    previousMetricHour = metricHour
}
if isMetricHapticsOn && metricHour != previousMetricHour {
    WKInterfaceDevice.current().play(.success)
    previousMetricHour = metricHour
}

This small addition ensures reliability in haptic triggers.

Section 3: UI and UX Enhancements

WatchOS UI should be glanceable and intuitive on small screens.

Change 3.1: Add Digital Crown Support for Tab Navigation

Description of the Change: Add .focusable() and .digitalCrownRotation to the TabView for crown-based navigation.

Reason for the Change: Apple Watch's Digital Crown is a key input method. Enabling it for tab switching improves accessibility and UX, especially for one-handed use. This follows WatchOS HIG for interactive elements.

What the Change Does: .focusable() allows crown focus; .digitalCrownRotation binds to selectedTab, incrementing/decrementing on rotation. Users can scroll through tabs smoothly without swiping, making navigation more natural. The binding ensures the tab updates in real-time with crown input.

Code Snippet:

In body:

TabView(selection: $selectedTab) {
    // tabs
}
.focusable()
.digitalCrownRotation($selectedTab, from: 1, through: 4, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true)

This adds haptic feedback on tab changes, tying into the app's haptic theme.

Change 3.2: Improve Night Mode Color Consistency

Description of the Change: Define colors as constants or use Color assets, and apply night mode more uniformly, e.g., to backgrounds and borders.

Reason for the Change: Hardcoded hex strings are repeated, risking inconsistencies. Using named colors from Assets.xcassets or constants simplifies changes and supports dark/light modes if expanded. Uniform application ensures the red theme doesn't miss elements, improving visual coherence.

What the Change Does: Define nightModeColor = Color(hex: "ea3323") and dayModeColor = Color(hex: "685b4f"). Replace all .foregroundColor(isNightModeOn ? Color(hex: "ea3323") : Color(hex: "685b4f")) with .foregroundColor(isNightModeOn ? nightModeColor : dayModeColor). For backgrounds, ensure .colorMultiply applies to all relevant images. This centralizes color logic, making theme switches (e.g., adding blue mode) easier.

Code Snippet:

Add at top:

let nightModeColor = Color(hex: "ea3323")
let dayModeColor = Color(hex: "685b4f")
let borderColor = Color(hex: "171717")

Then use throughout, e.g.:

.foregroundColor(isNightModeOn ? nightModeColor : dayModeColor)

This refactoring reduces repetition and errors.

Change 3.3: Animate Sun/Moon Rotation

Description of the Change: Add .animation(.linear(duration: 1), value: fractionOfDay) to the rotationEffect.

Reason for the Change: The sun/moon jumps every second; animation smooths it, enhancing visual appeal. WatchOS supports lightweight animations without performance hits, making the clock more engaging.

What the Change Does: When fractionOfDay updates, the rotation animates linearly over 1 second, creating a continuous motion illusion. This ties into the time association feature, helping users visualize day progress fluidly.

Code Snippet:

.rotationEffect(Angle(degrees: -360 * -fractionOfDay), anchor: .center)
.animation(.linear(duration: 1), value: fractionOfDay)

Simple yet impactful for UX.

Section 4: Performance Optimizations

The timer fires every second, which is fine but can be optimized.

Change 4.1: Optimize Timer for Minute Updates

Description of the Change: Change timer to fire every 36 seconds (since metric second is 0.864 standard seconds, but approximate for efficiency), and calculate exact fraction in render().

Reason for the Change: Every-second updates are unnecessary for a clock that changes visibly every metric second (~0.864s), but WatchOS battery is precious. Reducing frequency saves power without losing accuracy, as haptics trigger on hour boundaries.

What the Change Does: Timer publishes every 36 seconds (standard minute / 100 metric seconds approx), but render() uses Date() for precise fractionOfDay. Haptics check remains accurate. This halves update rate, improving battery life while maintaining functionality.

Code Snippet:

let timer = Timer.publish(every: 36, on: .main, in: .common).autoconnect()

Note: For exactness, keep 1s if battery isn't an issue, but this is a trade-off.

Change 4.2: Use LazyVStack in Info Views

Description of the Change: Replace VStack with LazyVStack in TextContentView.

Reason for the Change: VStack loads all content at once; LazyVStack loads on-demand, better for scrollable content on memory-limited WatchOS.

What the Change Does: As user scrolls, sentences load lazily, reducing initial render time and memory. The scrollTransition still applies, but performance improves for longer texts.

Code Snippet:

LazyVStack(alignment: .leading, spacing: 40) {
    // image and ForEach
}

Enhances responsiveness.

Section 5: Accessibility Improvements

Accessibility ensures the app is usable by all.

Change 5.1: Add Accessibility Labels and Hints

Description of the Change: Add .accessibilityLabel and .accessibilityHint to key elements like time text, toggles, and images.

Reason for the Change: WatchOS VoiceOver users need descriptions. This complies with Apple's accessibility guidelines, broadening audience.

What the Change Does: For time: .accessibilityLabel("Metric time: \(timeString(from: fractionOfDay))") reads the time aloud. Toggles get hints like "Toggles night mode on or off". Images get labels like "Sun and moon indicator". VoiceOver navigates and announces, improving inclusivity.

Code Snippet:

For time Text:

Text(timeString(from: fractionOfDay))
    .accessibilityLabel("Current metric time: \(hh) hours, \(mm) minutes, \(ss) seconds")

Similarly for others.

Change 5.2: Support Dynamic Type

Description of the Change: Use .font(.body) or scalable fonts instead of fixed sizes.

Reason for the Change: Users with larger text settings expect scaling. Fixed sizes ignore this, reducing accessibility.

What the Change Does: .font(.system(.body)) scales with user settings. App text adjusts, ensuring readability for vision-impaired users.

Code Snippet:

.font(.system(.body))

Replace fixed sizes.

Section 6: Error Handling and Robustness

Minimal errors, but add safeguards.

Change 6.1: Handle Date Component Edge Cases

Description of the Change: In Date extension, use guard for components.

Reason for the Change: Calendar.current can fail in rare locales; safeguards prevent crashes.

What the Change Does: If component fails, return 0 or log. Ensures app stability.

Code Snippet:

var hours: Int {
    let component = Calendar.current.component(.hour, from: self)
    return component >= 0 ? component : 0
}

Change 6.2: Add Version Check in About

Description of the Change: Safely unwrap Bundle version.

Reason for the Change: If key missing, app crashes; optional chaining prevents.

What the Change Does: Displays "unknown" if nil.

Code Snippet:

v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown")

Section 7: Additional Features

To elevate the app.

Change 7.1: Add Metric to Standard Time Converter

Description of the Change: Add a new tab with inputs to convert between times.

Reason for the Change: Educates users, adds utility.

What the Change Does: User enters metric time, sees standard equivalent, and vice versa. Uses similar calculations.

Code Snippet: New ConverterView, add to TabView as tag 5.

Change 7.2: Integrate Complications

Description of the Change: Add WKApplicationDelegate for complications showing metric time.

Reason for the Change: Allows glanceable time on watch face.

What the Change Does: Provides data for CLKComplication, updating metric time.

This covers major changes. Implement iteratively for best results.

Uodated App File Structure

MetricEdition/
├── MetricEditionApp.swift          # Main app entry (unchanged)
├── ContentView.swift              # Slimmed main coordinator
├── Views/
│   ├── ClockFaceView.swift        # Extracted clock display [attached_file:1]
│   ├── SettingsView.swift         # Extracted settings toggles [attached_file:1]
│   ├── TextContentView.swift      # Reusable info pages [attached_file:1]
│   └── ConverterView.swift        # New metric/standard converter [attached_file:1]
├── Models/
│   └── MetricTimeModel.swift      # Shared time logic & constants [attached_file:1]
└── Assets.xcassets                # Add color assets (nightModeColor, dayModeColor)
Terminal ~ @leejohnson.it
_

Let's Connect

Let’s bring your ideas to life. Whether you're envisioning a game-changing  Apple Watch app or a transformative iOS experience, I'm here to make it happen. Reach out today, and let's create something extraordinary together.

0/1000
Your details are used only to reply to your enquiry.

Latest Articles

Build your first Apple Watch app

How to Build Your First Apple Watch App: A Detailed Step-by-Step Guide

Building your first Apple Watch app can seem daunting, but with Swift and SwiftUI, it's more accessible than ever. This comprehensive guide, written as your dedicated Swift and SwiftUI tutor, will walk you through every step—from setting up your development environment to deploying a fully functional app.

MetricEdition Update

Enhancing the MetricEdition Apple Watch App: A Comprehensive Guide

The app code is well-structured for a beginner's project, demonstrating a solid grasp of SwiftUI basics, including state management, timers, and WatchKit integrations for haptics. However, as with any app, there is room for improvement in areas such as code modularity, user experience (UX), performance optimization, accessibility, persistence of user settings, and adherence to modern SwiftUI best practices.

Optimizing Battery Efficiency

Optimising Battery Efficiency: Techniques and Tools for Lightweight iOS and watchOS Apps

Techniques and tools for keeping your iOS and watchOS apps lightweight and power-efficient. Discover how background tasks, animations, and refresh cycles impact performance, and learn straightforward tweaks that keep apps smooth without draining user batteries.

Designing for Apple Watch

Designing for Apple Watch: Crafting Impactful Experiences with Swift and SwiftUI

Learn how to craft experiences for the smallest display with maximum impact, from micro-interactions to accessibility in tiny viewports where space and user attention are critical. Explore real advice for balancing minimal UI with functional richness and delight.

Swift References

Explore an A-Z directory of essential Swift terms, from core syntax like “async/await” to standard library staples and SwiftUI integrations. Each entry delivers clear definitions, practical code examples, and usage tips tailored for iOS and watchOS developers. This reference empowers you to master Swift’s nuances quickly, with balanced coverage across letters for consistent depth.

Comprehensive yet Focused: Key concepts alphabetized for fast lookup.

Hands-On Learning: Real-world snippets you can copy-paste, building on your SwiftUI app-building experience.

A–F

G–L

M–R

S–Z