This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
SundialKit v2.0.0 is a Swift 6.1+ communications library for Apple platforms with a modern three-layer architecture:
Layer 1: Core Protocols (SundialKitCore, SundialKitNetwork, SundialKitConnectivity)
- Protocol-based abstractions over Apple's Network and WatchConnectivity frameworks
- Minimal concurrency annotations (Sendable constraints)
- No observer patterns - pure wrappers
Layer 2: Observation Plugins (Choose your concurrency model)
- SundialKitStream: Modern actor-based observers with AsyncStream APIs
- SundialKitCombine: @MainActor-based observers with Combine publishers
Key Features:
- Network connectivity monitoring using Apple's Network framework
- WatchConnectivity abstraction for iPhone/Apple Watch communication
- Multiple concurrency models: Choose actors or @MainActor based on your needs
- Swift 6.1 strict concurrency compliant (zero @unchecked Sendable in plugins)
- Full async/await support alongside Combine publishers
make build # Build the package
make test # Run tests with coverage
make lint # Run linting and formatting (strict mode)
make format # Format code only
make clean # Clean build artifacts
make help # Show all available commandsswift build
swift test
swift test --enable-code-coverage
swift test --filter <TestName>SundialKit uses mise to manage development tools:
- swift-format (swiftlang/swift-format@602.0.0) - Official Apple Swift formatter
- SwiftLint (realm/SwiftLint@0.61.0) - Swift style and conventions linter
- Periphery (peripheryapp/periphery@3.2.0) - Unused code detection
curl https://mise.run | sh
# or
brew install misemise install # Installs tools from .mise.toml./Scripts/lint.sh # Normal mode
LINT_MODE=STRICT ./Scripts/lint.sh # Strict mode (CI)
FORMAT_ONLY=1 ./Scripts/lint.sh # Format onlySundialKit v2.0.0 uses a layered architecture separating protocols, wrappers, and observation patterns:
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: SundialKitCore (Protocols) │
│ - PathMonitor, NetworkPing, ConnectivitySession protocols │
│ - Sendable-safe type aliases and errors │
│ - No observers, no concurrency primitives │
└─────────────────────────────────────────────────────────────┘
│
┌────┴────┐
│ │
┌───┴────────┐ ┌──┴──────────────┐
│ Layer 1: │ │ Layer 1: │
│ Network │ │ Connectivity │
│ │ │ │
│ Raw │ │ Raw │
│ wrappers │ │ wrappers │
│ over │ │ over │
│ NWPath │ │ WCSession │
│ Monitor │ │ │
└────────────┘ └─────────────────┘
│ │
└────┬─────────────┘
│
┌────┴────┐
│ │
┌───┴────────┐│ ┌──────────────┐
│ Layer 2: ││ │ Layer 2: │
│ Stream ││ │ Combine │
│ ││ │ │
│ Actors + ││ │ @MainActor + │
│ AsyncStream││ │ Publishers │
│ (modern) ││ │ (SwiftUI) │
└────────────┘│ └──────────────┘
SundialKitCore (Sources/SundialKitCore/)
- Protocol definitions:
PathMonitor,NetworkPing,ConnectivitySession,ConnectivitySessionDelegate - Type-safe aliases:
ConnectivityMessage = [String: any Sendable] - Typed errors:
ConnectivityError,NetworkError,SerializationError - No concrete implementations, no observers
SundialKitNetwork (Sources/SundialKitNetwork/)
- Concrete implementations:
NWPathMonitorextensions,NeverPing - Protocol wrappers around Apple's Network framework
- PathStatus: Enum representing network state (satisfied, unsatisfied, requiresConnection, unknown)
- Contains
InterfaceOptionSet (cellular, wifi, wiredEthernet, loopback, other) - Contains
UnsatisfiedReasonenum for failure details
- Contains
SundialKitConnectivity (Sources/SundialKitConnectivity/)
- Concrete implementations:
WatchConnectivitySession,NeverConnectivitySession - Protocol wrappers around Apple's WatchConnectivity framework
- Delegate pattern support via
ConnectivitySessionDelegate - Message encoding/decoding via
Messagableprotocol
SundialKitStream (Packages/SundialKitStream/) - Modern Async/Await
-
NetworkObserver: Actor-based network monitoring
- Generic over
PathMonitorandNetworkPingprotocols - AsyncStream APIs:
pathUpdates(),pathStatusStream,isExpensiveStream,pingStatusStream - Call
start(queue:)to begin monitoring - Zero @unchecked Sendable (naturally Sendable actors)
- Generic over
-
ConnectivityObserver: Actor-based WatchConnectivity
- AsyncStream APIs:
activationStates(),messageStream(),reachabilityStream() - Async methods:
activate(),sendMessage(_:)returnsConnectivitySendResult - Automatic message routing (sendMessage when reachable, updateApplicationContext when not)
- Zero @unchecked Sendable (naturally Sendable actors)
- AsyncStream APIs:
SundialKitCombine (Packages/SundialKitCombine/) - Combine + SwiftUI
-
NetworkObserver: @MainActor-based network monitoring
- Generic over
PathMonitorandNetworkPing & Sendableprotocols - @Published properties:
pathStatus,isExpensive,isConstrained,pingStatus - Call
start(queue:)to begin monitoring (defaults to.main) - Zero @unchecked Sendable (@MainActor isolation)
- Generic over
-
ConnectivityObserver: @MainActor-based WatchConnectivity
- @Published properties:
activationState,isReachable,isPairedAppInstalled - PassthroughSubject publishers:
messageReceived,sendResult - Async methods:
activate(),sendMessage(_:)returnsConnectivitySendResult - Automatic message routing
- Zero @unchecked Sendable (@MainActor isolation)
- @Published properties:
Messagable Protocol
- Allows type-safe message encoding/decoding for WatchConnectivity
- Requires:
static key(type identifier),init?(from:),parameters() - Extension provides
message()method to convert toConnectivityMessage([String:Any])
MessageDecoder
- Initialize with array of
Messagable.Type - Builds internal dictionary keyed by
Messagable.key - Use
decode(_:)to convertConnectivityMessageback to typedMessagable
- All tests in
Tests/SundialKitTests/ - Testing Framework: Swift Testing (requires Swift 6.1+) - v2.0.0 migration from XCTest
- Mock implementations prefixed with "Mock" (MockSession, MockPathMonitor, MockNetworkPing, MockMessage)
- Protocol-based abstractions enable dependency injection for testing
- Never* types (NeverPing, NeverConnectivitySession) used for platforms without certain capabilities
- Protocol-oriented: Core types are protocols (
PathMonitor,NetworkPing,ConnectivitySession) - Platform availability: Heavy use of
#if canImport(Combine)and@availableattributes - Reactive: All state changes published via Combine publishers
- PassthroughSubject extension: Custom
anyPublisher(for:)helper maps KeyPath to publisher
- Swift Version: Swift 6.1+ required (v2.0.0+)
- Deployment Targets: iOS 16, watchOS 9, tvOS 16, macOS 11.0
- Requires Combine framework (macOS 10.15+, iOS 13+, watchOS 6+, tvOS 13+)
- WatchConnectivity only available on iOS and watchOS (not macOS/tvOS)
- Note: v2.0.0 dropped support for Swift 5.9, 5.10, and 6.0 to enable Swift Testing framework migration
Development tools (formatter, linter, unused code detector) are managed via mise and defined in .mise.toml. The Scripts/lint.sh script orchestrates formatting, linting, and code quality checks. Use make lint for local development.
ConnectivityMessage=[String: any Sendable](Sendable-safe WatchConnectivity dictionary)ConnectivityHandler=@Sendable (ConnectivityMessage) -> VoidSuccessfulSubject<Output>=PassthroughSubject<Output, Never>(in SundialKitCore for legacy support)
import SundialKitStream
import SundialKitNetwork
// Create observer (actor-based)
let observer = NetworkObserver(
monitor: NWPathMonitorAdapter(),
ping: nil // or provide a NetworkPing implementation
)
// Start monitoring
observer.start(queue: .global())
// Consume path updates using AsyncStream
Task {
for await status in observer.pathStatusStream {
print("Network status: \(status)")
}
}
// Or get raw path updates
Task {
for await path in observer.pathUpdates() {
print("Path: \(path)")
}
}import SundialKitCombine
import SundialKitNetwork
import Combine
// Create observer (@MainActor-based)
let observer = NetworkObserver(
monitor: NWPathMonitorAdapter(),
ping: nil
)
// Start monitoring on main queue
observer.start()
// Use @Published properties in SwiftUI
var cancellables = Set<AnyCancellable>()
observer.$pathStatus
.sink { status in
print("Network status: \(status)")
}
.store(in: &cancellables)
observer.$isExpensive
.sink { isExpensive in
print("Is expensive: \(isExpensive)")
}
.store(in: &cancellables)import SundialKitStream
import SundialKitConnectivity
// Create observer (actor-based)
let observer = ConnectivityObserver()
// Activate session
try await observer.activate()
// Listen for messages using AsyncStream
Task {
for await result in observer.messageStream() {
switch result.context {
case .replyWith(let handler):
print("Message: \(result.message)")
handler(["response": "acknowledged"])
case .applicationContext:
print("Context update: \(result.message)")
}
}
}
// Send messages
let result = try await observer.sendMessage(["key": "value"])
print("Sent via: \(result.context)")import SundialKitCombine
import SundialKitConnectivity
import Combine
// Create observer (@MainActor-based)
let observer = ConnectivityObserver()
// Activate session
try observer.activate()
// Use publishers
var cancellables = Set<AnyCancellable>()
observer.messageReceived
.sink { result in
switch result.context {
case .replyWith(let handler):
print("Message: \(result.message)")
handler(["response": "acknowledged"])
case .applicationContext:
print("Context update: \(result.message)")
}
}
.store(in: &cancellables)
observer.$activationState
.sink { state in
print("State: \(state)")
}
.store(in: &cancellables)
// Send messages asynchronously
Task {
let result = try await observer.sendMessage(["key": "value"])
print("Sent via: \(result.context)")
}- Observers require explicit start/activate: Both NetworkObserver and ConnectivityObserver need
start(queue:)oractivate()calls - Platform-specific APIs: WatchConnectivity guarded with
@availableand#if(behavior differs on iOS vs watchOS) - Messages must be property list types: ConnectivityMessage values must be Sendable property list types
- Actor isolation: When using SundialKitStream, remember to use
awaitfor actor-isolated properties and methods - Main thread access: When using SundialKitCombine, all observer access is on MainActor (safe for UI updates)
SundialKit v2.0.0 uses a modular Swift Package Manager architecture:
Repository Structure:
- Main Repository (
brightdigit/SundialKit): Contains SundialKitCore, SundialKitNetwork, SundialKitConnectivity, SundialKit umbrella, and built-in features (SundialKitBinary, SundialKitMessagable) - Plugin Packages: SundialKitStream and SundialKitCombine are distributed as separate Swift packages
brightdigit/SundialKitStream(tag: 1.0.0-alpha.1) - Modern async/await observersbrightdigit/SundialKitCombine(tag: 1.0.0-alpha.1) - Combine-based observers
Package Dependencies:
SundialKit uses standard Swift Package Manager dependencies. The Package.swift file references plugin packages as remote dependencies:
dependencies: [
.package(url: "https://github.com/brightdigit/SundialKitStream.git", from: "1.0.0-alpha.1"),
.package(url: "https://github.com/brightdigit/SundialKitCombine.git", from: "1.0.0-alpha.1")
]Working with Dependencies:
# Update all package dependencies
swift package update
# Resolve dependencies
swift package resolve
# Clean and rebuild
swift package clean
swift buildThis project uses GitHub Issues and Pull Requests with component-based organization:
- Each major feature gets a GitHub issue and feature branch
- Subtasks are tracked as task lists in the issue or as sub-issues
- Component/Package labeling is required for all issues and PRs
- Issue titles prefixed with component:
[Core] Feature: ...,[Network] Feature: ...,[WatchConnectivity] Feature: ... - GitHub labels applied:
component:core,component:network,component:watchconnectivity,component:combine,component:stream, etc.
- Issue titles prefixed with component:
- Feature branches follow the pattern:
feature/[component-]<description> - Commit messages reference component:
feat(core): description (#issue-number) - Pull requests include component scope:
feat(core): Feature Description - Pull requests are created when work is complete, closing the related issue
Component Labels:
component:core- SundialKitCore protocols and typescomponent:network- SundialKitNetwork implementationcomponent:watchconnectivity- SundialKitConnectivity implementationcomponent:combine- SundialKitCombine plugin package (v1 compatibility)component:messagable- SundialKitMessagable built-in (v1 compatibility)component:stream- SundialKitStream plugin package (modern async/await)component:binary- SundialKitBinary built-in (modern serialization)component:infrastructure- Build, CI/CD, tooling, package managementcomponent:docs- Documentation and examplescomponent:tests- Testing infrastructure and Swift Testing migration