Trouble sending data using Network framework to server.

Hi all,

I'm just a beginner wanting to create a client on MacOs for controlling a server (Hyperdeck). After a lot of beginners tutorials about basic Swift and SwiftUI, I started working on the client app.

It should be quite basic: Open connection to server via TCP, and once a connection is established send easy text data to the server.

After searching a lot, I figured Apple's Network framework (NWframework) would be quite useful. I found some examples and with those examples, i compiled the code below.

Now when running the application, I'm able to setup a connection and send a first datastring to the server by calling the initClient function. But then i try to send another string of data by calling functions as stopPlayout or sendData, but nothing happens. When adding client.start() before the client.connection.send() in the functions, it opens a new connection to the server and sends the string. But I want the client to send the data over the first connection which is still open. Hyperdeck only allows 1 connection.

Anyone can see what I'm doing wrong here?
Thanks a lot!


@Eskimo, thanks for your answer on swift forum. I did execute a package recording, nothing is recorded when calling the send function. So apparently no data is send when calling the send function. When initializing a connection and sending the first string in one function, packets are recorded.


Client.swift

import Foundation
import Network

@available(macOS 10.14, *)
class Client {
    let connection: ClientConnection
    let host: NWEndpoint.Host
    let port: NWEndpoint.Port

    init(host: String, port: UInt16) {
        self.host = NWEndpoint.Host(host)
        self.port = NWEndpoint.Port(rawValue: port)!
        let nwConnection = NWConnection(host: self.host, port: self.port, using: .tcp)
        connection = ClientConnection(nwConnection: nwConnection)
    }

    func start() {
        print("Client started \(host) \(port)")
        connection.didStopCallback = didStopCallback(error:)
        connection.start()
    }

    func stop() {
        connection.stop()
    }

    func send(data: Data) {
        connection.send(data: data)
    }

    func didStopCallback(error: Error?) {
        if error == nil {
            exit(EXIT_SUCCESS)
        } else {
            exit(EXIT_FAILURE)
        }
    }
}


ClientConnection.swift

import Foundation
import Network

@available(macOS 10.14, *)
class ClientConnection {

    let  nwConnection: NWConnection
    let queue = DispatchQueue(label: "Client connection Q")

    init(nwConnection: NWConnection) {
        self.nwConnection = nwConnection
    }

    var didStopCallback: ((Error?) -> Void)? = nil

    func start() {
        print("connection will start")
        nwConnection.stateUpdateHandler = stateDidChange(to:)
        setupReceive()
        nwConnection.start(queue: queue)
    }

    private func stateDidChange(to state: NWConnection.State) {
        switch state {
        case .waiting(let error):
            connectionDidFail(error: error)
        case .ready:
            print("Client connection ready")
        case .failed(let error):
            connectionDidFail(error: error)
        default:
            break
        }
    }

    private func setupReceive() {
        nwConnection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { (data, _, isComplete, error) in
            if let data = data, !data.isEmpty {
                let message = String(data: data, encoding: .utf8)
                print("connection did receive, data: \(data as NSData) string: \(message ?? "-" )")
            }
            if isComplete {
                self.connectionDidEnd()
            } else if let error = error {
                self.connectionDidFail(error: error)
            } else {
                self.setupReceive()
            }
        }
    }

    func send(data: Data) {
        nwConnection.send(content: data, completion: .contentProcessed( { error in
            if let error = error {
                self.connectionDidFail(error: error)
                return
            }
                print("connection did send, data: \(data as NSData)")
        }))
    }

    func stop() {
        print("connection will stop")
        stop(error: nil)
    }

    private func connectionDidFail(error: Error) {
        print("connection did fail, error: \(error)")
        self.stop(error: error)
    }

    private func connectionDidEnd() {
        print("connection did end")
        self.stop(error: nil)
    }

    private func stop(error: Error?) {
        self.nwConnection.stateUpdateHandler = nil
        self.nwConnection.cancel()
        if let didStopCallback = self.didStopCallback {
            self.didStopCallback = nil
            didStopCallback(error)
        }
    }
}


ContentView.swift

import Foundation
import SwiftUI
import Network

struct ContentView: View {
   
    @State var ipAdresHyperdeck1: String = "localhost"
    @State var ipAdresHyperdeck2: String = ""
    @State private var commandValue: String = ""
    @State var dataValue: String = ""
    let portHyperdeck: UInt16 = 8888
   
    var body: some View {
    VStack {
        HStack {
            Text("IP Hyperdeck 1")
                .padding(.leading, 60.0)
            TextField("IP adres", text: $ipAdresHyperdeck1)
                .frame(width: 90.0)
            Spacer()
        }
        HStack {
            Button(action: {
                self.initClient(server: "\(self.ipAdresHyperdeck1)", port: self.portHyperdeck)})
            {   Text("     Connect Hyperdeck 1     ")}
                .padding(.leading, 60)
        Spacer()
            Button(action: {
                self.stopConnection(server: "\(self.ipAdresHyperdeck1)", port: self.portHyperdeck)})
            {   Text("    Stop hyperdeck 1    ")}
                .padding(.trailing, 55)
        }
           
            Button(action: {
                self.stopPlayout(server: "\(self.ipAdresHyperdeck1)", port: self.portHyperdeck)})
            {Text("STOP")}
           
            Button(action: {
                self.playPlayout(server: "\(self.ipAdresHyperdeck1)", port: self.portHyperdeck)})
            {Text("PLAY")}
           
            Button(action: {
                self.goToInPlayout(server: "\(self.ipAdresHyperdeck1)", port: self.portHyperdeck)})
            {Text("GO TO IN")}

        HStack {
            Text("Data")
                    TextField("Data you want to send", text: $commandValue)
            Button(action: {
                self.sendData(server: "\(self.ipAdresHyperdeck1)", port: self.portHyperdeck)})
            {Text("    Send Data    ")}
        }
    }
}
   
func initClient(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    var command = ""
    client.start()
    switch (command){
        case "CRLF":
            command = "\r\n"
        case "RETURN":
            command = "\n"
        case "exit":
            client.stop()
        default:
            command = "hi server"
    }
        client.connection.send(data: (command.data(using: .utf8))!)
}

func stopConnection(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    client.stop()
}
       
func sendData(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    let command = String("\(commandValue)")
    switch (command) {
        case "exit":
            client.stop()
            commandValue = ""
        default:
            client.connection.send(data: (command.data(using: .utf8))!)
            commandValue = ""
    }
}
   
func stopPlayout(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    let command = String("stop")
    client.connection.send(data: (command.data(using: .utf8))!)
}
   
func playPlayout(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    let command = String("play")
    client.connection.send(data: (command.data(using: .utf8))!)
}
   
func goToInPlayout(server: String, port: UInt16) {
    let client = Client(host: server, port: port)
    let command = String("goto: clip: start")
    client.connection.send(data: (command.data(using: .utf8))!)
}
   

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
}

It looks like you have a previous thread going on Swift Forums with Quinn, so I will try not to barge in here too much. I just wanted to stop by and say that it looks like you are running in an issue where the client connection object setup in initClient is not being used to send data in stopPlayout, playPlayout, and goToInPlayout. Like you mentioned you are opening a new connection. If you are able to use this original connection object to send data on those other functions I'm betting you would have better luck. I will let a SwiftUI expert weigh in here on how best to do this.



Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Matt’s hint is spot on, but first let’s cover some general stuff…

First up, thanks for bouncing over to DevForums. As I mentioned on Swift Forums, your question is all about Apple frameworks (Network and SwiftUI) and thus a better match for here.

I recommend that you remove the queue you create an line 8 of

ClientConnection.swift
and instead switch to use
.main
on line 20. A secondary queue might make sense in your final product but, for bring up purposes, you definitely want to use the main queue for everything.

If you do decide to go back to a secondary queue, you need to think carefully about what queue your callbacks are executed on. For example, at some point you might want to wire your

didStopCallback
up to something that updates the UI, but our UI frameworks require that such updates be done on the main thread.

You wrote:

So apparently no data is send when calling the send function.

That’s not super unusual.

NWConnection
lets you send data before the connection is established. It buffers that data and sends it once the connection comes up.

As to what’s actually causing your problem, I think you need to look at the top level of your code, that is

ContentView
. This is a
struct
, so it has no long-term identity. In contrast, a network connection needs long-term identity, because the
NWConnection
reference you have is directly tied to a TCP connection on the ‘wire’.

Consider your

initClient(server:port:)
method. It creates the client object and starts it, and then just returns. At this point Swift’s runtime releases its last reference to the client object. That deallocates, taking the
NWConnection
with it. When you release the
NWConnection
, the underlying TCP connection goes away (or, more likely, is cancelled before it even gets started).

The solution here is to maintain an explicit reference to the client object. The standard way to do this is via an observed object property (

@ObservedObject
).

In a real app you’d probably want multiple levels of model object here, a top-level one that’s specifically structured to meet the needs of your view and a lower-level one that actually deals with the network. In your current test app, you can combine these, and just have

ContentView
maintain a reference to a
Client
object via a single
@ObservedObject
property.

To get you started, I created a concrete example. Here’s my model object:

import Foundation

class Client: ObservableObject {

    @Published
    var state: State = State(status: "Not started.", canStart: true, canStop: false)

    struct State {
        var status: String
        var canStart: Bool
        var canStop: Bool
    }

    private var timer: Timer? = nil

    func start() {
        guard self.timer == nil else { return }
        let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
            self.timerDidFire()
        }
        self.timer = timer
        self.state = State(status: "Started.", canStart: false, canStop: true)
    }

    func stop() {
        guard let timer = self.timer else { return }
        timer.invalidate()
        self.timer = nil
        self.state = State(status: "Stopped.", canStart: true, canStop: false)
    }

    private func timerDidFire() {
        self.timer = nil
        self.state = State(status: "Timed out.", canStart: true, canStop: false)
    }
}

This wraps a timer, not a network connection. I did this because timers are simpler to understand than network connections, but they are similar in two critical ways:

  • A timer has a persistent identity.

  • A timer can change state from ‘above’ (you clicking the Stop button) and from ‘below’ (the timer timing out).

Here’s how I wrapped that in a SwiftUI view:

struct ContentView: View {

    @ObservedObject
    var client: Client

    var body: some View {
        VStack {
            Text(client.state.status)
            Button(action: { self.client.start() }, label: { Text("Start") } )
                .disabled(!client.state.canStart)
            Button(action: { self.client.stop() }, label: { Text("Stop") } )
                .disabled(!client.state.canStop)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

As you can see, the view has no timer logic, it’s entirely driven by the state published by the

Client
observed object.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Wow, thank you for your extensive reply. Currently I'm at work, but will dive in to this tonight (Europe time).

Apparently working for national television combined with the Corona virus doesn't offer a lot of free time for coding... I will read myself in to observed objects and your advice when I find some time. If I'm getting stuck I hope I can get back to you!


Thanks for now!

Hi,

I was dealing with a similar problem than the OP, and wanted to test the proposed Timer based concrete example. So i've created a new Swift/SwiftUI project, added a Client.swift file with code, but as soon as i try to add the following code in ContentView.swift:

    @ObservedObject
    var client: Client

i get a "Failed to build scheme" error in UI preview, and a "Missing argument for parameter 'client' in call" error located at the

ContentView()

line near the end.

I've managed to silence these errors with this modification:

    @ObservedObject
    var client = Client()

The code then seems to work has expected.

I also tried to use the original syntax, and to add a (maybe missing) init block below, but did not managed to get rid of the errors.

Sorry if this is a dumb question, but could somebody clarify what the problem is with the original code (maybe some change with newer Swift version?), and what are the solutions to correct it (and if my correction if correct)?

Xcode 14.0.1/Swift 5.7

Thanks!

Additional question: is there a reason to specify "self" in

Button(action: { self.client.start() }, label: { Text("Start") } )
                .disabled(!client.state.canStart)

It seems to work also without it.

These are more Swift and SwiftUI questions than networking questions. You could, for example, replace the Client class with something that just prints values and you’d have the same issue. I’ll try to answer your questions here but, honestly, you’d be better off discussing this with SwiftUI experts.


I've managed to silence these errors with this modification:

That’s not the right fix here. The Client object is an input to your ContentView, and so you need to pass it in from whoever constructs the view. Constructing it in the view will cause all sorts of problems because your views are built and rebuilt many times over the lifetime of your app, but your networking client needs to be stable.

So, assuming you start with the iOS > App template, your view would look like this:

import SwiftUI

class Client: ObservableObject {
}

struct ContentView: View {
    var client: Client
    var body: some View {
        … as per the template …
    }
}

struct ContentView_Previews: PreviewProvider {
    static let client = Client()
    static var previews: some View {
        ContentView(client: client)
    }
}

You then have to tweak your app to look like this:

@main
struct Test130207App: App {
    @StateObject var client = Client()
    var body: some Scene {
        WindowGroup {
            ContentView(client: client)
        }
    }
}

Note the use of @StateObject, which ensures that the client object is preserved across view rebuilds. If you’re not familiar with that, I strongly recommend that you watch WWDC 2020 Session 10040 Data Essentials in SwiftUI, which discusses the fundamentals of how to manage state in a SwiftUI app.


is there a reason to specify self

As a matter of style, I use self everywhere (due to exposure to Python at an early age :-). Other folk have different styles. The self is not necessary here because of SE-0269.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks a lot for your complete and precise reply!

These are more Swift and SwiftUI questions than networking questions.

Right, but since i was trying to apply the example code provided in this thread, i thought it would be better to reply here. Sorry if it was a bad choice.

Ok! That makes sense for me now. I thought that the provided code samples were complete and usable "as is", but that was a bad assumption from me.

As a matter of style, I use self everywhere

Ok. I was pretty sure it doesn't hurt to have it or not, but wanted to be sure that i didn't miss something... like i did for the previous question!

I'm just starting at Swift development, and was also not very aware of object oriented development in C/C++, so i have a lot to learn here, and since Apple documentation is quite light on code examples, it's easy to get lost or confused, or to miss something obvious.

Thanks for your precious help. I will be able to continue to try to code my first macOS swift/swiftUI app to send commands to a serial device connected through an serial to IP converter (so using TCP connection)...

Trouble sending data using Network framework to server.
 
 
Q