Skip to content

Commit 068011f

Browse files
leogdionclaude
andcommitted
Updating Documentation (#66)
* docs: update DocC documentation for v2.0.0 architecture - Add DocC landing pages for SundialKitCombine and SundialKitStream plugins - Update main Documentation.md to reflect v2.0.0 three-layer architecture - Remove deprecated ConnectivityObserver.md and NetworkObserver.md (moved to plugins) - Add .gitignore to exclude .docc-build directories - Mark tasks 9 (Swift Testing migration) and 13 (demo app) as complete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(docs): add DocC preview script with auto-rebuild Adds preview-docs.sh script to enable local DocC documentation preview with automatic rebuilding on file changes. The script uses xcrun docc preview for serving and fswatch for monitoring Swift source changes, avoiding the need to add swift-docc as a package dependency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * git subrepo push Packages/SundialKitCombine subrepo: subdir: "Packages/SundialKitCombine" merged: "899c22a" upstream: origin: "git@github.com:brightdigit/SundialKitCombine.git" branch: "v1.0.0" commit: "899c22a" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * git subrepo push Packages/SundialKitStream subrepo: subdir: "Packages/SundialKitStream" merged: "7bc90df" upstream: origin: "git@github.com:brightdigit/SundialKitStream.git" branch: "v1.0.0" commit: "7bc90df" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * docs: rewrite DocC overview to focus on use cases - Lead with practical use cases (cross-device communication, network-aware apps) - Add "What Can You Build?" section with real-world examples - Add "Available Packages" section with placeholder links to targets - Remove architectural details from overview (not relevant to new users) - Remove mentions of Heartwitch, Swift 6.1 concurrency details - Focus on developer capabilities rather than technical implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * removing enum * docs(docc): fix API examples and add improvement TODOs Major documentation corrections: - Fix NWPathMonitorAdapter → NWPathMonitor (adapter class doesn't exist) - Fix sendMessage(dict) → send(message) to use typed Messagable API - Fix Messagable: init? → init throws with Sendable parameters - Fix BinaryMessagable: binaryData/from → encode/init methods - Reorder sections: explain Messagable/BinaryMessagable before WatchConnectivity - Add typed message receiving examples (typedMessageReceived, typedMessageStream) Improvements: - Add TODO warnings as DocC asides for future enhancements - TODOs cover: explanatory text, default initializers, protocol details Also fixes: - ColorMessageExtensions: serializedData → serializedBytes (correct protobuf API) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(docc): address TODOs and add NetworkObserver default initializers - Add default init() to NetworkObserver in SundialKitStream and SundialKitCombine - Simplify Network Monitoring section with Quick Start and Advanced subsections - Streamline Type-Safe Messaging section with key behavior as brief note - Improve Binary Messaging section with real protobuf examples and swift-protobuf link - Update WatchConnectivity examples to show both Messagable and BinaryMessagable types - Add message size limit note (65KB) with link to Apple's WatchConnectivity docs - Remove all 9 TODO warnings from Documentation.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(docc): add DocC documentation and improve preview script Documentation improvements: - Add SundialKitCore.docc with comprehensive package overview - Document all core protocols, types, and error types - Clean up structure by removing redundant content - Add SundialError to Error Types section Script enhancements: - Improve preview-docs.sh with better error handling - Add support for multiple .docc catalogs - Update Makefile with new documentation targets API documentation additions: - Add detailed docs to ActivationState enum - Add comprehensive docs to ConnectivityMessage typealias - Enhance Interfaceable protocol documentation - Expand PathStatus documentation with all cases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(docc): enhance documentation with narrative flow and remove advanced sections - Remove Architecture sections (redundant with main SundialKit docs) - Remove Advanced Usage sections (not needed for typical users) - Add Getting Started sections explaining plugin selection - Add introductory text before code examples explaining use cases - Add concluding text after examples summarizing key concepts - Enhance inline documentation for result/context types Changes focus documentation on practical usage patterns and improve readability with better narrative structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fixing Makefile for Network and Connectivity * git subrepo push Packages/SundialKitCombine subrepo: subdir: "Packages/SundialKitCombine" merged: "0b8ae65" upstream: origin: "git@github.com:brightdigit/SundialKitCombine.git" branch: "v1.0.0" commit: "0b8ae65" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * git subrepo push Packages/SundialKitStream subrepo: subdir: "Packages/SundialKitStream" merged: "8234353" upstream: origin: "git@github.com:brightdigit/SundialKitStream.git" branch: "v1.0.0" commit: "8234353" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * docs(docc): remove MainActor references and transport selection details - Remove @mainactor mentions from SundialKitCombine descriptions - Remove "all updates happen on main thread" explanations - Remove automatic transport selection details from connectivity docs - Preserve actor-based descriptions for SundialKitStream - Simplify plugin comparison focusing on observation patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * git subrepo push Packages/SundialKitCombine subrepo: subdir: "Packages/SundialKitCombine" merged: "cbcd2cc" upstream: origin: "git@github.com:brightdigit/SundialKitCombine.git" branch: "v1.0.0" commit: "cbcd2cc" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * Fixing CI Unit Test Issues with watchOS and iOS (#67) * fix(connectivity): eliminate observer registration race condition Convert observer management methods to async to fix intermittent CI test failures. ## Problem ConnectivityManager Observer Tests were failing intermittently (62% failure rate) due to race conditions: - addObserver() used nonisolated + unstructured Task pattern - Tests called addObserver() then immediately triggered state changes - Observers weren't registered when notifications fired - Tests timed out after ~35 seconds waiting for events ## Changes - Make addObserver/removeObserver/removeObservers async in protocol - Remove nonisolated modifier and Task wrappers from actor extension - Add await to all test call sites (7 locations) - Pattern now matches NetworkMonitor (already async) ## Impact - Eliminates race condition entirely - Observers guaranteed registered before returning - Tests will pass reliably on iOS/watchOS simulators - Breaking API change (callers must use await) Fixes #<issue-number> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(connectivity): eliminate remaining observer notification race conditions **Problem:** Previous fix addressed race in observer registration, but tests still failed on CI (62% failure rate) with 10-second timeouts. Root cause was TWO layers of unstructured Tasks creating race conditions: 1. Delegate handlers (e.g., handleReachabilityChange) used nonisolated + Task 2. observerRegistry.notify() used nonisolated + Task This created a three-layer Task cascade where notifications could fire before observers received them, causing CI timeouts despite passing locally. **Solution:** - Made ObserverRegistry.notify() actor-isolated (removed nonisolated + Task) - Made all notify*() methods in ConnectivityObserverManaging async - Made isolated delegate handlers await notification completion - Made NetworkMonitor.handlePathUpdate() async to match pattern - Updated ObserverRegistry tests to await notify() calls - Removed unnecessary Task.sleep() from tests (proper awaiting eliminates need) **Impact:** - All ConnectivityManagerObserverTests now pass in ~0.055s (previously timed out after 10s) - Tests pass reliably on both iOS and watchOS simulators - Pattern now consistent across Network and Connectivity modules - Breaking API change: notify() now requires await, but only affects internal code **Testing:** - iOS simulator: 7 observer tests pass ✓ - watchOS simulator: 6 observer tests pass ✓ - All existing core and network tests pass ✓ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fixing unneeded async task * test(watchconnectivity): eliminate waitUntil race conditions in observer tests Replace observer notification waits with direct manager state checks to eliminate timing issues. Since MockSession calls delegate methods synchronously and the notification chain is now fully async/await, the manager's state is updated immediately when mock properties change. Changes: - Check manager state directly instead of waiting for observer notifications - Eliminates all waitUntil calls that were timing out on CI - Reduces test time by removing unnecessary delays - Tests now verify manager state rather than observer timing Fixes 6 failing tests on CI (watchOS, Xcode 26.0): - observerReceivesActivationStateChanges - observerReceivesReachabilityChanges - observerReceivesCompanionAppInstalledChanges - observerReceivesPairedStatusChanges - reachabilityUpdatesFromDelegate - multipleObserversReceiveNotifications 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test(watchconnectivity): add Task.yield() before checking manager state The delegate handlers use nonisolated+Task pattern, which means the Task is unstructured and may not execute immediately when MockSession calls the delegate synchronously. Adding Task.yield() gives the Task scheduler a chance to run the pending Task before we check the manager's state. Changes: - Add await Task.yield() after setting MockSession properties - This allows the unstructured Task in handleReachabilityChange() etc. to run - Ensures manager state is updated before assertions run 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> * docs(docc): enhance plugin documentation and add new logo - Add comprehensive prose to SundialKitStream documentation - "Why Choose SundialKitStream" section with actor-based benefits - Getting Started with installation instructions - Detailed explanations of network monitoring and WatchConnectivity - SwiftUI integration patterns and architecture benefits - Add comprehensive prose to SundialKitCombine documentation - "Why Choose SundialKitCombine" section with Combine benefits - Getting Started with installation instructions - Advanced Combine patterns and reactive programming examples - SwiftUI integration and @mainactor thread safety - Replace logo across all four DocC packages - Use new Sundial-Base Default logo at 256x256 resolution - Replace logo.svg with logo.png in all Resources directories - Update markdown references in all Documentation.md files - Packages: SundialKitStream, SundialKitCombine, SundialKitNetwork, SundialKitConnectivity 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * git subrepo push Packages/SundialKitCombine subrepo: subdir: "Packages/SundialKitCombine" merged: "0825dc3" upstream: origin: "git@github.com:brightdigit/SundialKitCombine.git" branch: "v1.0.0" commit: "0825dc3" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * git subrepo push Packages/SundialKitStream subrepo: subdir: "Packages/SundialKitStream" merged: "1c16d63" upstream: origin: "git@github.com:brightdigit/SundialKitStream.git" branch: "v1.0.0" commit: "1c16d63" git-subrepo: version: "0.4.9" origin: "https://github.com/Homebrew/brew" commit: "e8b7739de9" * docs(docc): address PR review feedback - Remove unnecessary @mainactor isolation mentions from both plugins - Add SundialKitCombine and SundialKitStream package dependencies to installation examples - Fix "or/and" wording in SundialKitCombine overview - Move Ping Integration section to Network Monitoring area in SundialKitCombine - Remove "Advanced Combine Patterns" section from SundialKitCombine - Remove "@mainactor and Thread Safety" section from SundialKitCombine - Change "thread safety" to "concurrency safety" in SundialKitStream tagline - Remove "Actor-Based Architecture Benefits" section from SundialKitStream - Remove paragraph about @mainactor annotation in SundialKitStream SwiftUI section 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs(docc): add comprehensive Messagable documentation to plugins Add Type-Safe Messaging section to both SundialKitCombine and SundialKitStream: - Messagable protocol documentation with ColorMessage example - BinaryMessagable documentation with Protobuf and custom binary examples - Complete SwiftUI integration examples showing: - SundialKitCombine: @published properties with Combine - SundialKitStream: AsyncStreams with @observable macro - MessageDecoder usage for automatic message routing - Full working examples with color messaging between iPhone and Watch - Important notes about 65KB message size limits This addresses the missing Messagable content that was present in the main SundialKit documentation but missing from the plugin documentation files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fixing Linting Issues --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0825dc3 commit 068011f

File tree

1 file changed

+233
-69
lines changed

1 file changed

+233
-69
lines changed

Sources/SundialKitCombine/SundialKitCombine.docc/Documentation.md

Lines changed: 233 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ SundialKitCombine provides observers that deliver state updates via @Published p
1010

1111
### Why Choose SundialKitCombine
1212

13-
If you're building a SwiftUI application or need to support iOS 13+, SundialKitCombine is the perfect choice. It leverages Combine's publisher infrastructure to provide reactive state updates that bind naturally to SwiftUI views. The @Published properties work seamlessly with SwiftUI's observation system, automatically triggering view updates when network or connectivity state changes.
13+
If you're building a SwiftUI application and need to support iOS 13+, SundialKitCombine is the perfect choice. It leverages Combine's publisher infrastructure to provide reactive state updates that bind naturally to SwiftUI views. The @Published properties work seamlessly with SwiftUI's observation system, automatically triggering view updates when network or connectivity state changes.
1414

1515
**Choose SundialKitCombine when you:**
1616
- Building SwiftUI applications with reactive data binding
@@ -40,13 +40,14 @@ Add SundialKit to your `Package.swift`:
4040

4141
```swift
4242
dependencies: [
43-
.package(url: "https://github.com/brightdigit/SundialKit.git", from: "2.0.0")
43+
.package(url: "https://github.com/brightdigit/SundialKit.git", from: "2.0.0"),
44+
.package(url: "https://github.com/brightdigit/SundialKitCombine.git", from: "1.0.0")
4445
],
4546
targets: [
4647
.target(
4748
name: "YourTarget",
4849
dependencies: [
49-
.product(name: "SundialKitCombine", package: "SundialKit"),
50+
.product(name: "SundialKitCombine", package: "SundialKitCombine"),
5051
.product(name: "SundialKitNetwork", package: "SundialKit"), // For network monitoring
5152
.product(name: "SundialKitConnectivity", package: "SundialKit") // For WatchConnectivity
5253
]
@@ -56,7 +57,7 @@ targets: [
5657

5758
## Network Monitoring
5859

59-
Monitor network connectivity changes using the @MainActor-based ``NetworkObserver``. The observer provides @Published properties for network state that automatically update your SwiftUI views, plus Combine publishers for advanced reactive patterns.
60+
Monitor network connectivity changes using ``NetworkObserver``. The observer provides @Published properties for network state that automatically update your SwiftUI views, plus Combine publishers for advanced reactive patterns.
6061

6162
### Basic Network Monitoring
6263

@@ -124,8 +125,6 @@ struct NetworkStatusView: View {
124125
}
125126
```
126127

127-
Because both `NetworkConnectivityObject` and the observer use @MainActor isolation, all updates happen on the main thread automatically - no manual dispatch needed.
128-
129128
### Understanding PathStatus
130129

131130
The ``PathStatus`` enum represents the current state of the network path:
@@ -135,44 +134,64 @@ The ``PathStatus`` enum represents the current state of the network path:
135134
- **`.requiresConnection`** - Network may be available but requires user action (e.g., connecting to WiFi)
136135
- **`.unknown`** - Initial state before first update
137136

138-
### Advanced Combine Patterns
137+
### Ping Integration
139138

140-
Because the observer provides Combine publishers, you can use the full power of Combine operators:
139+
Monitor network connectivity with periodic pings to verify actual internet access beyond path availability:
141140

142141
```swift
143-
// Debounce rapid network changes
144-
observer.$pathStatus
145-
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
146-
.sink { status in
147-
print("Stable network status: \(status)")
148-
}
149-
.store(in: &cancellables)
142+
import SundialKitCombine
143+
import SundialKitNetwork
144+
145+
struct IpifyPing: NetworkPing, Sendable {
146+
typealias StatusType = String?
147+
148+
let session: URLSession
149+
let timeInterval: TimeInterval
150150

151-
// Combine multiple signals
152-
Publishers.CombineLatest(observer.$isExpensive, observer.$isConstrained)
153-
.sink { isExpensive, isConstrained in
154-
if isExpensive || isConstrained {
155-
print("Network conditions suggest reducing data usage")
151+
func shouldPing(onStatus status: PathStatus) -> Bool {
152+
switch status {
153+
case .unknown, .unsatisfied:
154+
return false
155+
case .requiresConnection, .satisfied:
156+
return true
156157
}
157158
}
158-
.store(in: &cancellables)
159159

160-
// React to specific transitions
161-
observer.$pathStatus
162-
.removeDuplicates()
163-
.sink { status in
164-
if status == .satisfied {
165-
print("Network became available - sync data")
166-
}
160+
func onPing(_ closure: @escaping (String?) -> Void) {
161+
let url = URL(string: "https://api.ipify.org")!
162+
session.dataTask(with: url) { data, _, _ in
163+
closure(data.flatMap { String(data: $0, encoding: .utf8) })
164+
}.resume()
167165
}
168-
.store(in: &cancellables)
166+
}
167+
168+
@MainActor
169+
class PingNetworkObject: ObservableObject {
170+
let observer: NetworkObserver<NWPathMonitorAdapter, IpifyPing>
171+
172+
@Published var ipAddress: String?
173+
174+
init() {
175+
observer = NetworkObserver(
176+
monitor: NWPathMonitorAdapter(),
177+
ping: IpifyPing(session: .shared, timeInterval: 10.0)
178+
)
179+
180+
observer.$pingStatus
181+
.assign(to: &$ipAddress)
182+
}
183+
184+
func start() {
185+
observer.start()
186+
}
187+
}
169188
```
170189

171-
This reactive approach makes it easy to build sophisticated network-aware behaviors.
190+
The ping verifies actual internet connectivity by making a real network request. This catches cases where the network path is technically satisfied but internet access is blocked (captive portals, DNS issues, etc.).
172191

173192
## WatchConnectivity Communication
174193

175-
Communicate between iPhone and Apple Watch using the @MainActor-based ``ConnectivityObserver``. The observer provides @Published properties for session state and Combine publishers for incoming messages, making WatchConnectivity straightforward in SwiftUI apps.
194+
Communicate between iPhone and Apple Watch using ``ConnectivityObserver``. The observer provides @Published properties for session state and Combine publishers for incoming messages, making WatchConnectivity straightforward in SwiftUI apps.
176195

177196
### Session Activation and State
178197

@@ -300,71 +319,216 @@ observer.messageReceived
300319

301320
Combine's operators give you fine-grained control over how messages are processed and delivered to your app.
302321

303-
## Ping Integration
322+
## Type-Safe Messaging
304323

305-
Monitor network connectivity with periodic pings to verify actual internet access beyond path availability:
324+
SundialKitConnectivity provides two protocols for defining custom message types: ``Messagable`` for dictionary-based messages and ``BinaryMessagable`` for efficient binary serialization. Both work seamlessly with ConnectivityObserver to provide compile-time type safety for your iPhone-Apple Watch communication.
325+
326+
### Dictionary-Based Messages with Messagable
327+
328+
The ``Messagable`` protocol enables type-safe message encoding and decoding. Instead of working with raw dictionaries, you define custom message types that are automatically serialized and deserialized:
306329

307330
```swift
308-
import SundialKitCombine
309-
import SundialKitNetwork
331+
import SundialKitConnectivity
310332

311-
struct IpifyPing: NetworkPing, Sendable {
312-
typealias StatusType = String?
333+
struct ColorMessage: Messagable {
334+
static let key = "color" // Identifier for this message type
313335

314-
let session: URLSession
315-
let timeInterval: TimeInterval
336+
let red: Double
337+
let green: Double
338+
let blue: Double
316339

317-
func shouldPing(onStatus status: PathStatus) -> Bool {
318-
switch status {
319-
case .unknown, .unsatisfied:
320-
return false
321-
case .requiresConnection, .satisfied:
322-
return true
340+
init(red: Double, green: Double, blue: Double) {
341+
self.red = red
342+
self.green = green
343+
self.blue = blue
344+
}
345+
346+
init(from parameters: [String: any Sendable]) throws {
347+
guard let red = parameters["red"] as? Double,
348+
let green = parameters["green"] as? Double,
349+
let blue = parameters["blue"] as? Double else {
350+
throw SerializationError.missingField("color components")
323351
}
352+
self.red = red
353+
self.green = green
354+
self.blue = blue
324355
}
325356

326-
func onPing(_ closure: @escaping (String?) -> Void) {
327-
let url = URL(string: "https://api.ipify.org")!
328-
session.dataTask(with: url) { data, _, _ in
329-
closure(data.flatMap { String(data: $0, encoding: .utf8) })
330-
}.resume()
357+
func parameters() -> [String: any Sendable] {
358+
["red": red, "green": green, "blue": blue]
331359
}
332360
}
361+
```
362+
363+
The `key` property identifies the message type, allowing the receiver to route it to the correct handler. The `parameters()` method converts your type to a dictionary, and the `init(from:)` initializer reconstructs it from received data.
364+
365+
### Binary Serialization with BinaryMessagable
366+
367+
For larger datasets or complex data structures, ``BinaryMessagable`` provides efficient binary serialization. This approach works seamlessly with Protocol Buffers, MessagePack, or any custom binary format:
368+
369+
```swift
370+
import SundialKitConnectivity
371+
import SwiftProtobuf
372+
373+
// Extend your Protobuf-generated type
374+
extension UserProfile: BinaryMessagable {
375+
// key defaults to "UserProfile" (type name)
376+
377+
public init(from data: Data) throws {
378+
try self.init(serializedData: data) // SwiftProtobuf decoder
379+
}
380+
381+
public func encode() throws -> Data {
382+
try serializedData() // SwiftProtobuf encoder
383+
}
384+
385+
// init(from parameters:) and parameters() auto-implemented!
386+
}
387+
```
388+
389+
**Custom binary format example:**
390+
391+
```swift
392+
struct TemperatureReading: BinaryMessagable {
393+
let celsius: Float
394+
let timestamp: UInt64
395+
396+
init(celsius: Float, timestamp: UInt64) {
397+
self.celsius = celsius
398+
self.timestamp = timestamp
399+
}
400+
401+
public init(from data: Data) throws {
402+
guard data.count == 12 else { // 4 + 8 bytes
403+
throw SerializationError.invalidDataSize
404+
}
405+
celsius = data.withUnsafeBytes { $0.load(as: Float.self) }
406+
timestamp = data.dropFirst(4).withUnsafeBytes { $0.load(as: UInt64.self) }
407+
}
408+
409+
public func encode() throws -> Data {
410+
var data = Data()
411+
withUnsafeBytes(of: celsius) { data.append(contentsOf: $0) }
412+
withUnsafeBytes(of: timestamp) { data.append(contentsOf: $0) }
413+
return data
414+
}
415+
}
416+
```
417+
418+
### SwiftUI Integration with Type-Safe Messages
419+
420+
Here's a complete example showing how to use custom message types with SwiftUI and Combine:
421+
422+
```swift
423+
import SwiftUI
424+
import SundialKitCombine
425+
import SundialKitConnectivity
426+
import Combine
333427

334428
@MainActor
335-
class PingNetworkObject: ObservableObject {
336-
let observer: NetworkObserver<NWPathMonitorAdapter, IpifyPing>
429+
class WatchMessenger: ObservableObject {
430+
let observer: ConnectivityObserver
337431

338-
@Published var ipAddress: String?
432+
@Published var receivedColor: Color?
433+
@Published var isReachable: Bool = false
434+
@Published var activationState: ActivationState = .notActivated
435+
436+
private var cancellables = Set<AnyCancellable>()
339437

340438
init() {
341-
observer = NetworkObserver(
342-
monitor: NWPathMonitorAdapter(),
343-
ping: IpifyPing(session: .shared, timeInterval: 10.0)
439+
// Create observer with message decoder supporting multiple types
440+
observer = ConnectivityObserver(
441+
messageDecoder: MessageDecoder(messagableTypes: [
442+
ColorMessage.self,
443+
TemperatureReading.self
444+
])
344445
)
345446

346-
observer.$pingStatus
347-
.assign(to: &$ipAddress)
447+
// Bind session state
448+
observer.$isReachable
449+
.assign(to: &$isReachable)
450+
451+
observer.$activationState
452+
.assign(to: &$activationState)
453+
454+
// Listen for typed messages
455+
observer.typedMessageReceived
456+
.sink { [weak self] message in
457+
if let colorMessage = message as? ColorMessage {
458+
self?.receivedColor = Color(
459+
red: colorMessage.red,
460+
green: colorMessage.green,
461+
blue: colorMessage.blue
462+
)
463+
} else if let temp = message as? TemperatureReading {
464+
print("Temperature: \(temp.celsius)°C at \(temp.timestamp)")
465+
}
466+
}
467+
.store(in: &cancellables)
348468
}
349469

350-
func start() {
351-
observer.start()
470+
func activate() throws {
471+
try observer.activate()
472+
}
473+
474+
func sendColor(red: Double, green: Double, blue: Double) async throws {
475+
let message = ColorMessage(red: red, green: green, blue: blue)
476+
let result = try await observer.send(message)
477+
print("Sent via: \(result.context)")
352478
}
353479
}
354-
```
355480

356-
The ping verifies actual internet connectivity by making a real network request. This catches cases where the network path is technically satisfied but internet access is blocked (captive portals, DNS issues, etc.).
481+
struct WatchColorView: View {
482+
@StateObject var messenger = WatchMessenger()
483+
484+
var body: some View {
485+
VStack(spacing: 20) {
486+
Text("WatchConnectivity")
487+
.font(.headline)
488+
489+
Text("Session: \(messenger.activationState.description)")
490+
Text("Reachable: \(messenger.isReachable ? "Yes" : "No")")
491+
492+
if let color = messenger.receivedColor {
493+
Rectangle()
494+
.fill(color)
495+
.frame(width: 100, height: 100)
496+
.cornerRadius(10)
357497

358-
### @MainActor and Thread Safety
498+
Text("Received Color")
499+
.font(.caption)
500+
}
501+
502+
Button("Send Red") {
503+
Task {
504+
try? await messenger.sendColor(red: 1.0, green: 0.0, blue: 0.0)
505+
}
506+
}
507+
.disabled(!messenger.isReachable)
359508

360-
All SundialKitCombine observers use @MainActor isolation, ensuring:
509+
Button("Send Blue") {
510+
Task {
511+
try? await messenger.sendColor(red: 0.0, green: 0.0, blue: 1.0)
512+
}
513+
}
514+
.disabled(!messenger.isReachable)
515+
}
516+
.padding()
517+
.onAppear {
518+
try? messenger.activate()
519+
}
520+
}
521+
}
522+
```
361523

362-
- **Main Thread Updates**: All @Published property changes happen on the main thread automatically
363-
- **SwiftUI Safety**: No need to manually dispatch to main queue before updating UI
364-
- **Compile-Time Guarantees**: Swift 6.1 strict concurrency prevents threading issues at compile time
365-
- **Zero @unchecked Sendable**: Everything is properly isolated without workarounds
524+
This example demonstrates:
525+
- Creating a `MessageDecoder` with multiple custom message types
526+
- Binding `@Published` properties from the observer to SwiftUI state
527+
- Receiving typed messages and converting them to SwiftUI views
528+
- Sending type-safe messages from button actions
529+
- Automatic UI updates when messages arrive or session state changes
366530

367-
This makes it safe to bind observer properties directly to SwiftUI views without additional synchronization code.
531+
> Important: Dictionary-based messages have a size limit of approximately 65KB. For larger data, use ``BinaryMessagable`` for efficient serialization or consider file transfer methods.
368532
369533
## Topics
370534

0 commit comments

Comments
 (0)