Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Walkable.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
4A200AD9B414882AC6C7C572 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
51D80C129C05029BAD7DE958 /* WalkableApp.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = WalkableApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
5515782EF765667BB5836D12 /* WalkableWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = WalkableWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
57B9A25E290E6711909B627B /* WalkableKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = WalkableKit; sourceTree = SOURCE_ROOT; };
57B9A25E290E6711909B627B /* WalkableKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = WalkableKit; path = WalkableKit; sourceTree = SOURCE_ROOT; };
57F061AAADC8839C2E49EE10 /* WalkTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalkTabView.swift; sourceTree = "<group>"; };
59A20463D956D4332A76E146 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
5DB7618EACDEED770EA1628E /* WalkableLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalkableLiveActivity.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -746,7 +746,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = WalkableWatch/WalkableWatch.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = HYM2LV4SPQ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Walkable;
INFOPLIST_KEY_NSHealthShareUsageDescription = "Walkable reads health data during walks.";
Expand Down Expand Up @@ -917,6 +916,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
Expand All @@ -929,6 +929,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -945,7 +946,6 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = WalkableWatch/WalkableWatch.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = HYM2LV4SPQ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = Walkable;
INFOPLIST_KEY_NSHealthShareUsageDescription = "Walkable reads health data during walks.";
Expand Down Expand Up @@ -1028,6 +1028,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
Expand All @@ -1046,6 +1047,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down
7 changes: 5 additions & 2 deletions WalkableApp/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI
import SwiftData
import HealthKit
import WalkableKit

struct ContentView: View {
Expand Down Expand Up @@ -162,7 +163,8 @@ struct ContentView: View {
guard !alreadyExists else { continue }

// Find the closest matching route by distance
let workoutDistance = workout.totalDistance?.doubleValue(for: .meter()) ?? 0
let distanceType = HKQuantityType(.distanceWalkingRunning)
let workoutDistance = workout.statistics(for: distanceType)?.sumQuantity()?.doubleValue(for: .meter()) ?? 0
guard let matchingRoute = allRoutes.min(by: { a, b in
abs(a.distance - workoutDistance) < abs(b.distance - workoutDistance)
}) else { continue }
Expand All @@ -172,7 +174,8 @@ struct ContentView: View {
session.completedAt = workout.endDate
session.totalDistance = workoutDistance
session.totalDuration = workout.duration
session.calories = workout.totalEnergyBurned?.doubleValue(for: .kilocalorie()) ?? 0
let energyType = HKQuantityType(.activeEnergyBurned)
session.calories = workout.statistics(for: energyType)?.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
session.source = "healthkit"

// Try to fetch the GPS route
Expand Down
26 changes: 26 additions & 0 deletions WalkableApp/Views/Library/LibraryView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SwiftUI
import SwiftData
import UniformTypeIdentifiers
import CoreLocation
import WatchConnectivity
import WalkableKit

struct LibraryView: View {
Expand All @@ -10,6 +11,8 @@ struct LibraryView: View {
@State private var viewModel = LibraryViewModel()
@ObservedObject private var locationService = LocationService.shared
@State private var showImportPicker = false
@State private var showSyncAlert = false
@State private var syncStatus = ""
@State private var importError: String?
@State private var showImportError = false

Expand Down Expand Up @@ -101,6 +104,24 @@ struct LibraryView: View {
Image(systemName: "square.and.arrow.down")
}
}
ToolbarItem(placement: .primaryAction) {
Button {
let session = WCSession.default
let status = """
Paired: \(session.isPaired)
Watch App Installed: \(session.isWatchAppInstalled)
Reachable: \(session.isReachable)
Activation: \(session.activationState.rawValue)
Routes: \(routes.count)
"""
syncStatus = status
SyncService.shared.syncAllRoutes(routes)
showSyncAlert = true
Haptics.success()
} label: {
Image(systemName: "arrow.triangle.2.circlepath")
}
}
}
.fileImporter(
isPresented: $showImportPicker,
Expand All @@ -113,6 +134,11 @@ struct LibraryView: View {
} message: {
Text(importError ?? "Could not read GPX file.")
}
.alert("Sync Status", isPresented: $showSyncAlert) {
Button("OK", role: .cancel) {}
} message: {
Text(syncStatus)
}
.sheet(item: $viewModel.selectedRoute) { route in
RouteDetailSheet(route: route) {
onStartWalk?(route)
Expand Down
26 changes: 21 additions & 5 deletions WalkableKit/Sources/WalkableKit/Services/HealthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,12 +189,12 @@ public final class HealthService: NSObject, ObservableObject {

guard let route = routes.first else { return [] }

let locationsBox = LocationsBox()
return try await withCheckedThrowingContinuation { continuation in
var locations = [CLLocation]()
let routeQuery = HKWorkoutRouteQuery(route: route) { _, batch, done, error in
if let error { continuation.resume(throwing: error); return }
if let batch { locations.append(contentsOf: batch) }
if done { continuation.resume(returning: locations) }
if let batch { locationsBox.append(batch) }
if done { continuation.resume(returning: locationsBox.all) }
}
store.execute(routeQuery)
}
Expand Down Expand Up @@ -301,13 +301,13 @@ public final class HealthService: NSObject, ObservableObject {
}

#if os(watchOS)
extension HealthService: @preconcurrency HKWorkoutSessionDelegate {
extension HealthService: HKWorkoutSessionDelegate {
public nonisolated func workoutSession(_ workoutSession: HKWorkoutSession, didChangeTo toState: HKWorkoutSessionState, from fromState: HKWorkoutSessionState, date: Date) {}

public nonisolated func workoutSession(_ workoutSession: HKWorkoutSession, didFailWithError error: Error) {}
}

extension HealthService: @preconcurrency HKLiveWorkoutBuilderDelegate {
extension HealthService: HKLiveWorkoutBuilderDelegate {
public nonisolated func workoutBuilderDidCollectEvent(_ workoutBuilder: HKLiveWorkoutBuilder) {}

public nonisolated func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
Expand Down Expand Up @@ -337,3 +337,19 @@ extension HealthService: @preconcurrency HKLiveWorkoutBuilderDelegate {
}
}
#endif

/// Thread-safe accumulator for concurrent HKWorkoutRouteQuery callbacks.
private final class LocationsBox: @unchecked Sendable {
private let lock = NSLock()
private var storage = [CLLocation]()

func append(_ batch: [CLLocation]) {
lock.lock(); defer { lock.unlock() }
storage.append(contentsOf: batch)
}

var all: [CLLocation] {
lock.lock(); defer { lock.unlock() }
return storage
}
}
2 changes: 1 addition & 1 deletion WalkableWatch/ViewModels/WatchWalkViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ final class WatchWalkViewModel {
.sink { [weak self] location in
guard let self else { return }
self.currentLocation = location.coordinate
self.distanceWalked = self.healthService.distanceWalked ?? 0
self.distanceWalked = self.healthService.distanceWalked

// Auto-re-center map if user hasn't swiped in 10 seconds
if self.hasZoomedIn && Date().timeIntervalSince(self.lastManualMapInteraction) > 10 {
Expand Down
5 changes: 5 additions & 0 deletions project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ options:
xcodeVersion: "16.0"
createIntermediateGroups: true

settings:
base:
CURRENT_PROJECT_VERSION: "1"
MARKETING_VERSION: "1.0"

packages:
WalkableKit:
path: WalkableKit
Expand Down
Loading