SwiftStreamManager

main

Run a binary backend using a native macOS SwiftUI frontend.
calmdocs/SwiftStreamManager

SwiftStreamManager

Note (March 2024)

While this library works as advertised, if used on a laptop that is often woken and put to sleep, after a long period of time the Swift websocket connection may fail, requiring an automatic restart of the golang binary. This is an issue with the underlying macOS Swift websockets library. Please consider using calmdocs/SwiftPollManager instead which communicates via http long polling.

About

As an alternative to building an electron app or using a gui toolkit, run a golang binary embedded in a native macOS SwiftUI app. The golang binary and SwiftUI app communicate via encrypted websocket messages.

If you already have xCode and go installed, following the example to build a new running app takes about 2 minutes.

Setup

Create a new macOS Swift Xcode project:

  • File -> Add Packages ... -> https://github.com/calmdocs/SwiftStreamManager
  • Select the checkbox at Target -> Signing & Capabilities -> App Sandbox -> Network -> Incoming connections (Server).
  • Select the checkbox at Target -> Signing & Capabilities -> App Sandbox -> Network -> Incoming connections (Client).

Example

Create our go binaries (for amd64 and arm64)

Run the following commands:

Drag the gobinary-darwin-amd64 and gobinary-darwin-arm64 files that we just built into our new macOS Swift xCode project.

In our new Swift xCode project, replace ContentView.swift with the following code:

import SwiftUI

import SwiftStreamManager
import SwiftWebSocketManager
import SwiftProcessManager

public struct CustomStorageItem: Codable, Identifiable {
    public var id: Int64
    
    let error: String?
    let name: String
    let status: String
    let progress: Double

    enum CodingKeys: String, CodingKey {
        case id = "ID"
        case error = "Error"
        case name = "Name"
        case status = "Status"
        case progress = "Progress"
    }
}

struct ContentView: View {
    @ObservedObject var itemsStore: ItemsStore = ItemsStore()
     
    var body: some View {
        List {
            HStack {
                Button(action: {
                    itemsStore.sm.publish(
                        itemsStore.sendStream!,
                        type: "addItem",
                        id: "",
                        data: ""
                    )
                }, label: {
                    Image(systemName: "plus")
                })
                Spacer()
            }
            ForEach(itemsStore.items) { item in
                HStack{
                    Text("\(item.name) (\(item.status))")
                    Spacer()
                    Button(action: {
                        itemsStore.sm.publish(
                            itemsStore.sendStream!,
                            type: "deleteItem",
                            id: String(item.id),
                            data: ""
                        )
                    }, label: {
                        Image(systemName: "trash")
                    })
                }
            }
        }
    }
}

class ItemsStore: ObservableObject {
    private let binName = SystemArchitecture() == "arm64" ? "gobinary-darwin-arm64" : "gobinary-darwin-amd64"

    @Published var items: [CustomStorageItem] = [CustomStorageItem]()
    
    // StreamManager
    @Published var sm: StreamManager
    @Published var sendStream: WebSocketStream?
  
    init() {
      
        // Initialise StreamManager
        self.sm = StreamManager(
            KeyExchange_Curve25519_SHA256_HKDF_AESGCM,
            baseURL: URL(string: "ws://127.0.0.1")!,
            port: Int.random(in: 8001..<9000)
        )
        self.sm.addPIDAsArgument("pid")
        self.sm.addBearerTokenAsArgument("token")
        self.sm.addPortAsArgument("port")
        
        // Start binary and subscribe to a websocket stream
        self.sm.subscribeWithBinary(
            streamPath: "/ws/0",
            binName: self.binName,
            withPEMWatcher: true,
            standardOutput: { result in
                print(result)
            },
            messages: { message in

                // Decrypt and decode
                guard let newItems: [CustomStorageItem] = try? self.sm.decryptAndDecodeJSON(
                    message: message,
                    auth: self.sm.authTimestamp  // Use the current time since 1970 in milliseconds as the default key exchange auth key.
                ) else {
                    return
                }

                // Update items
                DispatchQueue.main.async {
                    self.items.replaceInPlace(items: newItems)
                }
            },
            onConnected: {
                
                // Create a second (sending) stream subscribed to a different path
                DispatchQueue.main.async {
                    self.sendStream = try! self.sm.stream(streamPath: "/ws/1")
                    self.sm.subscribe(self.sendStream!,
                        //messages: { message in
                        //    print(message)
                        //},
                        errors: { err in
                            print(err)
                        }
                    )
                }
            }
        )
    }
}

Security

We have been as conservative as possible when creating this library. See the security details available on the calmdocs/SwiftKeyExchange package page. Please note that you use this library and the code in this repo at your own risk, and we accept no liability in relation to its use.

Description

  • Swift Tools 5.8.0
View More Packages from this Author

Dependencies

Last updated: Mon Apr 01 2024 21:00:24 GMT-0900 (Hawaii-Aleutian Daylight Time)