https://github.com/apparata/approach
Approach is a small Swift framework for client/server message passing between apps over the network.
https://github.com/apparata/approach
ios macos swift swift-package
Last synced: 3 months ago
JSON representation
Approach is a small Swift framework for client/server message passing between apps over the network.
- Host: GitHub
- URL: https://github.com/apparata/approach
- Owner: apparata
- License: 0bsd
- Created: 2018-10-01T21:39:10.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2025-06-08T12:33:23.000Z (about 1 year ago)
- Last Synced: 2025-10-30T07:37:38.374Z (8 months ago)
- Topics: ios, macos, swift, swift-package
- Language: Swift
- Homepage:
- Size: 55.7 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Approach
**Approach** is a Swift library for message passing between apps over the network. It supports both client and server roles, with optional Bonjour service discovery for zero-configuration local networking.
## License
Approach is available under the BSD Zero Clause License. One advantage of this license is that it does not require attribution. See the [LICENSE](LICENSE) file in the repository for details.
## Requirements
Minimum OS version requirements:
- iOS 16
- tvOS 16
- visionOS 1
- macOS 13 Ventura
## Features
- TCP message transmission with structured payloads
- Optional Bonjour advertisement and discovery
- Codable-based data and metadata messaging
- Asynchronous message reception using delegate callbacks
- Configurable logging support for diagnostics
## Installation
To use a dependency of another Swift package, add the following dependency to your `Package.swift` file:
```swift
.package(url: "https://github.com/yourusername/approach.git", from: "x.y.z")
```
- Replace `x.y.z` with the release version.
- Include `Approach` in your target dependencies.
## Example
### Define Messages
The messages to pass between apps are typically defined as `Codable` enums. This allows structured, type-safe messaging.
```swift
enum GeneralMessage: Codable {
case helloWorld
/// Client sends this to the server to ask about meaning of life
case whatsTheMeaningOfLife
/// Server sends this to the client with a number as the meaning of life
case meaningOfLife(Int)
}
```
Each message is accompanied by app specific metadata. The metadata should typically contain the type of the message, to inform the receiver about how the message should be decoded.
```swift
enum AppMessageMetadata: String, Codable {
/// Message should be decoded as `GeneralMessage`
case general
}
```
### Encoding / Decoding Messages
For the purposes of this example, we will add encoding/decoding helpers to the message types, to make the example code more readable.
```swift
extension GeneralMessage {
func encode() -> Data {
try! JSONEncoder().encode(self)
}
static func decode(from data: Data) -> GeneralMessage {
try! JSONDecoder().decode(GeneralMessage.self, from: data)
}
}
extension AppMessageMetadata {
func encode() -> Data {
try! JSONEncoder().encode(self)
}
static func decode(from data: Data) -> AppMessageMetadata {
try! JSONDecoder().decode(AppMessageMetadata.self, from: data)
}
}
```
### Setting up the Server
This example shows how to set up a server that advertises its presence on the local network using Bonjour, listens for incoming clients, performs connection handshakes, and handles structured messages.
```swift
import Foundation
import Approach
class ExampleServer: MessageServiceDelegate, RemoteMessageClientDelegate {
private let service: MessageService
init() throws {
// "MyService" is the Bonjour name to use for automatic discovery.
service = try MessageService(name: "MyService")
service.delegate = self
}
@discardableResult
func start() -> Self {
service.start()
return self
}
func messageService(
_ service: MessageService,
clientDidConnect remoteClient: RemoteMessageClient
) {
remoteClient.delegate = self
}
func clientDidStartSession(_ remoteClient: RemoteMessageClient) {
print("Session started with remote client.")
}
func client(
_ remoteClient: RemoteMessageClient,
didReceiveMessage data: Data,
metadata: Data
) {
let metadata = AppMessageMetadata.decode(from: metadata)
guard metadata == .general else {
print("Received unexpected metadata")
return
}
print("Server received a message of type: \(metadata.rawValue)")
let message = GeneralMessage.decode(from: data)
switch message {
case .helloWorld:
print("Client says hello. Say hello back.")
sendMessage(.helloWorld, to: remoteClient)
case .whatsTheMeaningOfLife:
print("Client wants to know what the meaning of life is. It is 42.")
sendMessage(.meaningOfLife(42), to: remoteClient)
default:
// Server does not care about e.g. the meaningOfLife message.
break
}
}
private func sendMessage(
_ message: GeneralMessage,
to remoteClient: RemoteMessageClient
) {
remoteClient.sendMessage(
data: message.encode(),
metadata: AppMessageMetadata.general.encode()
)
}
}
```
If you are starting the server from a command line tool, you probably want to start a `RunLoop`, or the process will terminate without waiting for any messages.
```swift
let exampleServer = ExampleServer().start()
RunLoop.main.run()
```
### Setting up the Client (Bonjour-based)
This example shows how to set up a client that looks for advertising servers on the local network using Bonjour, connects to the server, and sends structured messages.
```swift
import Foundation
import Approach
class ExampleClient: MessageClientDelegate {
private let client: MessageClient
init() {
// "MyService" is the Bonjour name to use for automatic discovery.
client = MessageClient(serviceName: "MyService")
client.delegate = self
}
@discardableResult
func connect() -> Self {
client.connect()
return self
}
func clientDidStartSession(_ client: MessageClient) {
// Send a couple of messages once the session has started.
sendMessage(.helloWorld, to: client)
sendMessage(.whatsTheMeaningOfLife)
}
func client(
_ client: MessageClient,
didReceiveMessage data: Data,
metadata: Data
) {
let metadata = AppMessageMetadata.decode(from: metadata)
guard metadata == .general else {
print("Invalid server message")
return
}
let message = GeneralMessage.decode(from: data)
print("Client received: \(message)")
}
func client(_ client: MessageClient, didFailSessionWithError error: Error) {
print("Connection failed: \(error)")
}
func sendMessage(_ message: GeneralMessage, to client: MessageClient) {
client.sendMessage(
data: message.encode(),
metadata: AppMessageMetadata.general.encode()
)
}
}
```
If you are starting the client from a command line tool, you probably want to start a `RunLoop`, or the process will terminate without waiting for any messages.
```swift
let client = ExampleClient().connect()
RunLoop.main.run()
```
### Setting up the Client (Direct IP Address)
To connect to a known server via IP and port, replace the `init` with:
```swift
init() {
// "MyService" is the Bonjour name to use for automatic discovery.
client = MessageClient(host: "192.168.1.10", port: 4242)
}
```
### Logging
Approach includes optional logging hooks to help with debugging and
development. You can assign a closure to `MessageClient.log` or
`RemoteMessageClient.log` to receive diagnostic output.
**Example**
```swift
MessageClient.log = { client, message in
print("[Client][\(client)] \(message)")
}
RemoteMessageClient.log = { client, message in
print("[Server][\(client.id)] \(message)")
}
```
Use this to observe connection state changes, message events, and internal
behavior during development.