Skip to content

kvaDrug/Connectivity

Repository files navigation

Connectivity

Lightweight iOS <-> watchOS synchronization built on WatchConnectivity.

Connectivity provides two ways to exchange data:

  • automatic state sync for ObservableObject models via ConnectivityCommunicator – useful for SwiftUI, and shared settings.
  • manual message transport via ConnectivitySender + ConnectivityReceiver – fast and robust multipath message delivery.

It is designed for settings/state replication where the newest value matters more than delivery of every intermediate event.

Features

  • Bidirectional iPhone/Watch communication (ConnectivitySender, ConnectivityReceiver)
  • Portable protocol for automatic model synchronization (ConnectivityCommunicator)
  • Manual messaging API with typed payloads (Codable)
  • Message routing by:
    • ConnectivityChannel (settings, watchLog)
    • ConnectivityCounterpart (iOS, watchOS)
  • Built-in latest-value reduction per key
  • Optional persisted message log/records (via FileStorage)

Requirements

  • Swift 6.2+ with concurrency
  • iOS 16+
  • watchOS 9+
  • Xcode with iOS/watchOS SDKs and WatchConnectivity

Installation (Swift Package Manager)

dependencies: [
    .package(url: "https://github.com/kvaDrug/Connectivity.git", branch: "main")
],
targets: [
    .target(
        name: "YourTarget",
        dependencies: [
            "Connectivity"
        ]
    )
]

Quick Start (Automatic ObservableObject Sync)

1. Define a portable model

import Connectivity

struct SharedSettingsPortable: Codable, Equatable {
    var premiumEnabled: Bool
    var refreshInterval: Int
}

@MainActor
final class SharedSettingsStore: ObservableObject, Portable {
    typealias PortableRepresentation = SharedSettingsPortable

    @Published var premiumEnabled = false
    @Published var refreshInterval = 15

    var portable: PortableRepresentation {
        .init(
            premiumEnabled: premiumEnabled,
            refreshInterval: refreshInterval
        )
    }

    func updateValues(with portable: PortableRepresentation) {
        premiumEnabled = portable.premiumEnabled
        refreshInterval = portable.refreshInterval
    }
}

2. Start communicator

import Connectivity

@MainActor
final class ConnectivityBootstrap {
    private let settings = SharedSettingsStore()
    private var communicator: ConnectivityCommunicator?

    func start() {
        ConnectivitySessionManager.shared.activate()
        communicator = ConnectivityCommunicator(
            observable: settings,
            key: .sharedSettings
        )
    }
}

ConnectivityCommunicator will:

  • send updates when objectWillChange emits
  • debounce outgoing updates (900ms by default)
  • receive newest remote value and apply it with updateValues(with:)

Manual Messaging

Use this when you want explicit messages instead of full model sync.

Send

import Connectivity

let sender = ConnectivitySender(sendingTo: .watchOS, via: .watchLog)
let key: ConnectivityMessageKey = "watch-log-line"

do {
    let message = try ConnectivityMessage(
        key: key,
        value: "App launched"
    )
    sender.send(message)
} catch {
    print("Failed to build ConnectivityMessage:", error)
}

Receive

import Connectivity
import Foundation

@MainActor
final class WatchLogReceiver {
    let receiver = ConnectivityReceiver(
        receiveIn: .iOS,
        via: .watchLog,
        uniqueKeys: false
    )

    private var token: NSObjectProtocol?

    func start() {
        token = NotificationCenter.default.addObserver(
            forName: ConnectivityReceiver.didReceiveDataNotification,
            object: receiver,
            queue: .main
        ) { [weak self] _ in
            guard let self else { return }
            print("Received messages:", self.receiver.log.count)
        }
    }
}

If uniqueKeys is true, use receiver.records to get newest value per key.

Message Model

ConnectivityMessage stores:

  • key: ConnectivityMessageKey
  • jsonValue: String (payload encoded from Codable)
  • timestamp: TimeInterval
  • destination: ConnectivityCounterpart?

You can define your own keys:

import Connectivity

extension ConnectivityMessageKey {
    static let accountState: Self = "account-state"
}

Public API Overview

  • Portable: protocol for automatic sync of ObservableObject
  • ConnectivityCommunicator: wires Portable to sender/receiver flow
  • ConnectivitySender: sends ConnectivityMessage to counterpart
  • ConnectivityReceiver: receives, merges, stores messages and posts updates
  • ConnectivitySessionManager: singleton managing WCSession activation/delegate
  • SessionDelegateMulticast: fan-out of WCSessionDelegate events as publishers
  • ConnectivityMessage, ConnectivityMessageKey, ConnectivityRecord
  • WCSession.canSend: helper for delivery eligibility

Combine is exported by this package, so AnyPublisher, @Published, etc. are available after importing Connectivity.

Concurrency and Threading

  • Core types are @MainActor where mutable shared state is managed. It's required by WatchConnectivity.
  • Keep ConnectivitySender, ConnectivityReceiver, and ConnectivityCommunicator usage on the main actor.

Delivery Semantics

  • Designed for "latest state wins" scenarios.
  • Messages may be delayed; do not rely on strict real-time delivery.
  • For long logs, duplicate keys are reduced to most recent message.
  • destination filtering prevents cross-target processing.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors