https://github.com/brightdigit/sundialkit
Communications library across Apple platforms.
https://github.com/brightdigit/sundialkit
apple-watch apple-watch-application network-analysis swift swift-package-manager watchkit-sdk
Last synced: about 1 month ago
JSON representation
Communications library across Apple platforms.
- Host: GitHub
- URL: https://github.com/brightdigit/sundialkit
- Owner: brightdigit
- License: mit
- Created: 2022-04-06T23:31:08.000Z (about 3 years ago)
- Default Branch: main
- Last Pushed: 2025-03-07T21:04:01.000Z (3 months ago)
- Last Synced: 2025-04-09T21:50:35.523Z (about 2 months ago)
- Topics: apple-watch, apple-watch-application, network-analysis, swift, swift-package-manager, watchkit-sdk
- Language: Swift
- Homepage: https://swiftpackageindex.com/brightdigit/SundialKit/0.2.0/documentation/sundialkit
- Size: 2.68 MB
- Stars: 28
- Watchers: 2
- Forks: 5
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
![]()
SundialKit
Reactive communications library across Apple platforms.
[](https://swift.org)
[](http://twitter.com/brightdigit)


[](https://swiftpackageindex.com/brightdigit/SundialKit)
[](https://swiftpackageindex.com/brightdigit/SundialKit)[](https://codecov.io/gh/brightdigit/SundialKit)
[](https://www.codefactor.io/repository/github/brightdigit/SundialKit)
[](https://codebeat.co/projects/github-com-brightdigit-SundialKit-main)
[](https://codeclimate.com/github/brightdigit/SundialKit)
[](https://codeclimate.com/github/brightdigit/SundialKit)
[](https://codeclimate.com/github/brightdigit/SundialKit)
[](https://houndci.com)
# Table of Contents
* [**Introduction**](#introduction)
* [**Features**](#features)
* [**Installation**](#installation)
* [**Usage**](#usage)
* [**Listening to Networking Changes**](#listening-to-networking-changes)
* [**Communication between iPhone and Apple Watch**](#communication-between-iphone-and-apple-watch)
* [**Connection Status**](#connection-status)
* [**Sending and Receiving Messages**](#sending-and-receiving-messages)
* [**Using Messagable to Communicate**](#using-messagable-to-communicate)
* [**License**](#license)# Introduction
For easier use in reactive user interfaces, especially with `SwiftUI` and `Combine`, I've created a library which abstracts and maps common connectivity APIs. Particularly in my app Heartwitch, I mapped the functionality of _WatchConnectivity_ and _Network_ over to track the user's ability to connect to the Internet as well as the ability for their iPhone to connect to their Apple Watch via _WatchConnectivity_
# Features
Here's what's currently implemented with this library:
- [x] Monitor network connectivity and quality
- [x] Communicate between iPhone and Apple Watch
- [x] Monitor connectivity between devices
- [x] Send messages back and forth between iPhone and Apple Watch
- [x] Abstract messages for easier _encoding_ and _decoding_# Installation
Swift Package Manager is Apple's decentralized dependency manager to integrate libraries to your Swift projects. It is now fully integrated with Xcode 13.
To integrate **SundialKit** into your project using SPM, specify it in your Package.swift file:
```swift
let package = Package(
...
dependencies: [
.package(url: "https://github.com/brightdigit/SundialKit.git", from: "0.2.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: ["SundialKit", ...]),
...
]
)
```# Usage
## Listening to Networking Changes
In the past `Reachability` or `AFNetworking` has been used to judge the network connectivity of a device. **SundialKit** uses the `Network` framework to listen to changes in connectivity providing all the information available.
**SundialKit** provides a `NetworkObserver` which allows you to listen to a variety of publishers related to the network. This is especially useful if you are using `SwiftUI` in particular. With `SwiftUI`, you can create an `ObservableObject` which contains a `NetworkObserver`:
```swift
import SwiftUI
import SundialKitclass NetworkConnectivityObject : ObservableObject {
// our NetworkObserver
let connectivityObserver = NetworkObserver()
// our published property for pathStatus initially set to `.unknown`
@Published var pathStatus : PathStatus = .unknowninit () {
// set the pathStatus changes to our published property
connectivityObserver
.pathStatusPublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$pathStatus)
}
// need to start listening
func start () {
self.connectivityObserver.start(queue: .global())
}
}
```There are 3 important pieces:
1. The `NetworkObserver` called `connectivityObserver`
2. On `init`, we use `Combine` to listen to the publisher and store each new `pathStatus` to our `@Published` property.
3. A `start` method which needs to be called to start listening to the `NetworkObserver`.Therefore for our `SwiftUI` `View`, we need to `start` listening `onAppear` and can use the `pathStatus` property in the `View`:
```swift
struct NetworkObserverView: View {
@StateObject var connectivityObject = NetworkConnectivityObject()
var body: some View {
// Use the `message` property to display text of the `pathStatus`
Text(self.connectivityObject.pathStatus.message).onAppear{
// start the NetworkObserver
self.connectivityObject.start()
}
}
}
```Besides `pathStatus`, you also have access to:
* `isExpensive`
* `isConstrained`### Verify Connectivity with ``NetworkPing``
In addition to utilizing `NWPathMonitor`, you can setup a periodic ping by implementing ``NetworkPing``. Here's an example which calls the _ipify_ API to verify there's an ip address:
```swift
struct IpifyPing : NetworkPing {
typealias StatusType = String?let session: URLSession
let timeInterval: TimeIntervalpublic func shouldPing(onStatus status: PathStatus) -> Bool {
switch status {
case .unknown, .unsatisfied:
return false
case .requiresConnection, .satisfied:
return true
}
}static let url : URL = .init(string: "https://api.ipify.org")!
func onPing(_ closure: @escaping (String?) -> Void) {
session.dataTask(with: IpifyPing.url) { data, _, _ in
closure(data.flatMap{String(data: $0, encoding: .utf8)})
}.resume()
}
}
```Next, in our `ObservableObject`, we can create a ``NetworkObserver`` to use this with:
```swift
@Published var nwObject = NetworkObserver(ping:
// use the shared `URLSession` and check every 10.0 seconds
IpifyPing(session: .shared, timeInterval: 10.0)
)
```## Communication between iPhone and Apple Watch
Besides networking, **SundialKit** also provides an easier reactive interface into `WatchConnectivity`. This includes:
1. Various connection statuses like `isReachable`, `isInstalled`, etc..
2. Send messages between the iPhone and paired Apple Watch
3. Easy encoding and decoding of messages between devices into `WatchConnectivity` friendly dictionaries.
Let's first talk about how `WatchConnectivity` status works.
### Connection Status
With `WatchConnectivity` there's a variety of properties which tell you the status of connection between devices. Here's a similar example to `pathStatus` using `isReachable`:
```swift
import SwiftUI
import SundialKitclass WatchConnectivityObject : ObservableObject {
// our ConnectivityObserver
let connectivityObserver = ConnectivityObserver()
// our published property for isReachable initially set to false
@Published var isReachable : Bool = false
init () {
// set the isReachable changes to our published property
connectivityObserver
.isReachablePublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$isReachable)
}
func activate () {
// activate the WatchConnectivity session
try! self.connectivityObserver.activate()
}
}
```Again, there are 3 important pieces:
1. The `ConnectivityObserver` called `connectivityObserver`
2. On `init`, we use `Combine` to listen to the publisher and store each new `isReachable` to our `@Published` property.
3. An `activate` method which needs to be called to activate the session for `WatchConnectivity`.Therefore for our `SwiftUI` `View`, we need to `activate` the session at `onAppear` and can use the `isReachable` property in the `View`:
```swift
struct WatchConnectivityView: View {
@StateObject var connectivityObject = WatchConnectivityObject()
var body: some View {
Text(
connectivityObject.isReachable ?
"Reachable" : "Not Reachable"
)
.onAppear{
self.connectivityObject.activate()
}
}
}
```Besides `isReachable`, you also have access to:
* `activationState`
* `isReachable`
* `isPairedAppInstalled`
* `isPaired`Additionally there's also a set of publishers for sending, receiving, and replying to messages between the iPhone and paired Apple Watch.
### Sending and Receiving Messages
To send and receive messages through our ``ConnectivityObserver`` we can access two properties:
- ``ConnectivityObserver/messageReceivedPublisher`` - for listening to messages
- ``ConnectivityObserver/sendingMessageSubject`` - for sending messages**SundialKit** uses `[String:Any]` dictionaries for sending and receiving messages, which use the typealias ``ConnectivityMessage``. Let's expand upon the previous `WatchConnectivityObject` and use those properties:
```swift
class WatchConnectivityObject : ObservableObject {// our ConnectivityObserver
let connectivityObserver = ConnectivityObserver()// our published property for isReachable initially set to false
@Published var isReachable : Bool = false// our published property for the last message received
@Published var lastReceivedMessage : String = ""init () {
// set the isReachable changes to our published property
connectivityObserver
.isReachablePublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$isReachable)// set the lastReceivedMessage based on the dictionary's _message_ key
connectivityObserver
.messageReceivedPublisher
.compactMap({ received in
received.message["message"] as? String
})
.receive(on: DispatchQueue.main)
.assign(to: &self.$lastReceivedMessage)
}
func activate () {
// activate the WatchConnectivity session
try! self.connectivityObserver.activate()
}func sendMessage(_ message: String) {
// create a dictionary with the message in the message key
self.connectivityObserver.sendingMessageSubject.send(["message" : message])
}
}
```We can now create a simple SwiftUI View using our updated `WatchConnectivityObject`:
```swift
struct WatchMessageDemoView: View {
@StateObject var connectivityObject = WatchMessageObject()
@State var message : String = ""
var body: some View {
VStack{
Text(connectivityObject.isReachable ? "Reachable" : "Not Reachable").onAppear{
self.connectivityObject.activate()
}
TextField("Message", text: self.$message)
Button("Send") {
self.connectivityObject.sendMessage(self.message)
}
Text("Last received message:")
Text(self.connectivityObject.lastReceivedMessage)
}
}
}
```### Using `Messagable` to Communicate
We can even abstract the ``ConnectivityMessage`` using a ``MessageDecoder``. To do this we need to create a special type which implements ``Messagable``:
```swift
struct Message : Messagable {
internal init(text: String) {
self.text = text
}
static let key: String = "_message"
enum Parameters : String {
case text
}
init?(from parameters: [String : Any]?) {
guard let text = parameters?[Parameters.text.rawValue] as? String else {
return nil
}
self.text = text
}
func parameters() -> [String : Any] {
return [
Parameters.text.rawValue : self.text
]
}
let text : String
}
```There are three requirements for implementing ``Messagable``:
* ``Messagable/init(from:)`` - try to create the object based on the dictionary, return nil if it's invalid
* ``Messagable/parameters()`` - return a dictionary with all the parameters need to recreate the object
* ``Messagable/key`` - return a string which identifies the type and is unique to the ``MessageDecoder``Now that we have our implementation of ``Messagable``, we can use it in our `WatchConnectivityObject`:
```swift
class WatchConnectivityObject : ObservableObject {// our ConnectivityObserver
let connectivityObserver = ConnectivityObserver()// create a `MessageDecoder` which can decode our new `Message` type
let messageDecoder = MessageDecoder(messagableTypes: [Message.self])// our published property for isReachable initially set to false
@Published var isReachable : Bool = false// our published property for the last message received
@Published var lastReceivedMessage : String = ""init () {
// set the isReachable changes to our published property
connectivityObserver
.isReachablePublisher
.receive(on: DispatchQueue.main)
.assign(to: &self.$isReachable)
connectivityObserver
.messageReceivedPublisher
// get the ``ConnectivityReceiveResult/message`` part of the ``ConnectivityReceiveResult``
.map(\.message)
// use our `messageDecoder` to call ``MessageDecoder/decode(_:)``
.compactMap(self.messageDecoder.decode)
// check it's our `Message`
.compactMap{$0 as? Message}
// get the `text` property
.map(\.text)
.receive(on: DispatchQueue.main)
// set it to our published property
.assign(to: &self.$lastReceivedMessage)
}
func activate () {
// activate the WatchConnectivity session
try! self.connectivityObserver.activate()
}func sendMessage(_ message: String) {
// create a dictionary using ``Messagable/message()``
self.connectivityObserver.sendingMessageSubject.send(Message(text: message).message())
}
}
```# License
This code is distributed under the MIT license. See the [LICENSE](https://github.com/brightdigit/SundialKit/LICENSE) file for more info.