Networking in iOS app v Mac command line tool

Hi,


I'm using SPM to extract the networking component of an app into a package. Part of what I thought I'd get, by doing that, is to be able to build a command-line-tool to make testing quicker. The tool is essentially like a ping command built on top of the network package, that I can use to test the networking logic while I simulator various conditions with the Cloud API (offline, busy, etc).


However, it's the first command line tool I've built. And I'm learning that I need to learn more about command line tool building 🙂


This is a fairly standard method to set the stateUpdateHandler (in my networking package). It works fine in the iOS simulator, but doesn't get called when used in the command line tool. But, I call a .send() after starting the connection and in both cases the data is sent to the remote side ('nc -l 80').


Can anyone recommend some good resources to learn, that will help me figure out why the below doesn't work on the command line?


thanks


    func startConnection() {

        guard let connection = connection else {
            return
        }
        
        // A handler that receives connection state updates.
        connection.stateUpdateHandler = { newState in
            switch newState {
            case .setup:
                print("setup: \(connection)")

            case .preparing:
                print("preparing: \(connection)")

            case .ready:
                print("established: \(connection)")

            case .waiting(let error):
                print("\(connection) waiting with \(error)")

            case .failed(let error):
                print("\(connection) failed with \(error)")

            default:
            break
            }
        }

        connection.start(queue: .main)
    }

Accepted Reply

Without your entire program it is difficult to say. To test it I created a Swift package from the command line:

$ mkdir NetworkCLI
$ cd NetworkCLI
$ swift package init --type executable
$ swift package generate-xcodeproj


Open up the .xcodeproj in Xcode and setup the main.swift file like so (Notice how I park the main thread with the technique that Quinn used):

import Foundation

class Main {
    
    var conn: Connection?
    static let shared = Main()
    
     func setup() {
        conn = Connection()
        conn?.delegate = self
        conn?.startConnection()
        
        dispatchMain()
    }


    func sendData() {
        for index in 1...3 {
            conn?.send(message: "test message \(index)")
        }
    }
}


extension Main: ConnectionDelegate {
    func connectionDidUpdate(_ conn: Connection, _ ready: Bool) {
        if ready {
            sendData()
        }
    }
}

Main.shared.setup()


Now create a connection class and set it up the way you previously had:

import Foundation
import Network




protocol ConnectionDelegate: AnyObject {
    func connectionDidUpdate(_ conn: Connection, _ ready: Bool)
}


class Connection: ConnectionDelegate {
    var connection: NWConnection?
    weak var delegate: ConnectionDelegate?
            
    func startConnection() {
        connection = NWConnection(host: "127.0.0.1", port: NWEndpoint.Port("8000")!, using: .tcp)
        connection?.stateUpdateHandler = { [weak self] newState in
            guard let strongSelf = self else { return }
            switch newState {
              case .setup:
                  print("Connection Setup")
        
              case .preparing:
                  print("Connection Preparing")
        
              case .ready:
                  print("Connection Established")
                  strongSelf.delegate?.connectionDidUpdate(strongSelf, true)
              case .waiting(let error):
                  print("Waiting with \(error)")
        
              case .failed(let error):
                  print("Failed with \(error)")
        
              default:
              break
            }
        }
        connection?.start(queue: .main)
        print("connection start")
    }
          
    func connectionDidUpdate(_ conn: Connection, _ state: Bool) {
        print("Did update state to ready: \(state), \(conn)")
    }
  
    func send(message: String) {
        print("sending '\(message)' while connection state is \(String(describing: connection?.state))")
        connection?.send(content: Data(message.utf8), completion: NWConnection.SendCompletion.contentProcessed { error in
            if error != nil {
                print("Error sending: \(error?.localizedDescription)")
            }
        })
    }
}

On the terminal listen on port 8000 with netcat:

$ nc -l 8000


Now either run your project from Xcode or run the executable from the commandline:

$ ./NetworkCLI


And you should see:

connection start
Connection Preparing
Connection Established
sending 'test message 1' while connection state is Optional(Network.NWConnection.State.ready)
sending 'test message 2' while connection state is Optional(Network.NWConnection.State.ready)
sending 'test message 3' while connection state is Optional(Network.NWConnection.State.ready)


With the listening end contains:

test message 1test message 2test message 3


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Replies

It sounds like you are on the right track; you could create a Library for your Networking needs and import that as a dependency. You could also just create XCTests for you networking work in your existing package. For more information on getting started using Swift PM, take a look at the Package Manager documentation, or Creating a Standalone Swift Package with Xcode.


For creating a Standalone Swift Package with Xcode you could try testing with something like this:

@available(OSX 10.14, *)
class Connection {
    var connection: NWConnection?
    weak var delegate: ConnectionDelegate?
        
    func startConnection() {
        connection = NWConnection(host: "127.0.0.1", port: NWEndpoint.Port("8000")!, using: .tcp)
        connection?.stateUpdateHandler = { [weak self] newState in
            guard let strongSelf = self else { return }
            switch newState {
              case .setup:
                  print("Connection setup")
    
              case .preparing:
                  print("Connection preparing")
    
              case .ready:
                  print("Connection Established")
                  strongSelf.delegate?.connectionDidUpdate(connection: strongSelf, ready: true)
              case .waiting(let error):
                  print("Waiting with \(error)")
    
              case .failed(let error):
                  print("Failed with \(error)")
    
              default:
              break
            }
        }
        connection?.start(queue: .main)
    }
}


import XCTest
@testable import NetworkTest


final class NetworkTestTests: XCTestCase {
    
    var expectation: XCTestExpectation?
    
    override func setUp() {
         expectation = XCTestExpectation(description: "NetworkTest")
    }
    
    @available(OSX 10.14, *)
    func testExample() {
        
        let conn = Connection()
        conn.delegate = self
        conn.startConnection()
        wait(for: [expectation!], timeout: 5.0)
    }


    @available(OSX 10.14, *)
    static var allTests = [
        ("testExample", testExample),
    ]
}


@available(OSX 10.14, *)
extension NetworkTestTests: ConnectionDelegate {
    func connectionDidUpdate(connection: Connection, ready: Bool) {
        if ready {
            expectation?.fulfill()
        }
    }
}


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Hi,


Thanks. The problem I'm having is the different behaviour when I run the above code (yours or mine) in an iOS app v an OSX command line tool.


So now, using your suggested code, I get different results between iOS and OSX. But with both I see no state change being reported. The examples all look the same, and I've looked at other networking frameworks. Is this something weird about running from Xcode?


I am using this test snippet:

let conn = Connection()
conn.startConnection()
sleep(10)
for index in 1...3 {
    conn.send(message: "test message \(index)")
}


And this is the code:

import Foundation
import Network

@available(OSX 10.14, *)
class Connection: ConnectionDelegate {
    var connection: NWConnection?
    weak var delegate: ConnectionDelegate?
          
    func startConnection() {
        connection = NWConnection(host: "127.0.0.1", port: NWEndpoint.Port("80")!, using: .tcp)
        connection?.stateUpdateHandler = { [weak self] newState in
            guard let strongSelf = self else { return }
            switch newState {
              case .setup:
                  print("Connection setup")
      
              case .preparing:
                  print("Connection preparing")
      
              case .ready:
                  print("Connection Established")
                  strongSelf.delegate?.connectionDidUpdate(strongSelf, true)
              case .waiting(let error):
                  print("Waiting with \(error)")
      
              case .failed(let error):
                  print("Failed with \(error)")
      
              default:
              break
            }
        }
        connection?.start(queue: .main)
        print("connection start")
    }
        
    func connectionDidUpdate(_ conn: Connection, _ state: Bool) {
        print("Did update state to ready: \(state), \(conn)")
    }

    func send(message: String) {
        print("sending '\(message)' while connection state is \(connection?.state)")
        connection?.send(content: Data(message.utf8), completion: NWConnection.SendCompletion.idempotent)
    }
}

protocol ConnectionDelegate: AnyObject {
    func connectionDidUpdate(_ conn: Connection, _ ready: Bool)
}


Console output when run as an iOS app:

connection start
sending 'test message 1' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 2' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 3' while connection state is Optional(Network.NWConnection.State.setup)


Running nc in Terminal:

% nc -l 80
test message 1test message 2test message 3%


Console output when run as an OSX command-line-tool:

connection start
sending 'test message 1' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 2' while connection state is Optional(Network.NWConnection.State.setup)
sending 'test message 3' while connection state is Optional(Network.NWConnection.State.setup)
Program ended with exit code: 0


Running nc in Terminal:

% nc -l 80
test message 1%

Have you tried using .contentProcessed on the send completion?


connection.send(content: Data(message.utf8), completion: NWConnection.SendCompletion.contentProcessed)



Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

One other thing to check on is to make sure the main thread is parked long enough to give your test time to complete. When creating a command line tool you can use dispatchMain() to do so. Checkout this post Quinn answered on how to do just that.


https://forums.developer.apple.com/thread/116723


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

Hi,


thanks for your help so far.


The question is why is stateUpdateHandler not being called? Even though, clearly, the connection is coming up, data is sent (and received), and the connection closes. I need to be able to have some logic take action if the state changes. This is what I'm trying to figure out.


cheers,

I'll take a look at this, and play around with the code.

Without your entire program it is difficult to say. To test it I created a Swift package from the command line:

$ mkdir NetworkCLI
$ cd NetworkCLI
$ swift package init --type executable
$ swift package generate-xcodeproj


Open up the .xcodeproj in Xcode and setup the main.swift file like so (Notice how I park the main thread with the technique that Quinn used):

import Foundation

class Main {
    
    var conn: Connection?
    static let shared = Main()
    
     func setup() {
        conn = Connection()
        conn?.delegate = self
        conn?.startConnection()
        
        dispatchMain()
    }


    func sendData() {
        for index in 1...3 {
            conn?.send(message: "test message \(index)")
        }
    }
}


extension Main: ConnectionDelegate {
    func connectionDidUpdate(_ conn: Connection, _ ready: Bool) {
        if ready {
            sendData()
        }
    }
}

Main.shared.setup()


Now create a connection class and set it up the way you previously had:

import Foundation
import Network




protocol ConnectionDelegate: AnyObject {
    func connectionDidUpdate(_ conn: Connection, _ ready: Bool)
}


class Connection: ConnectionDelegate {
    var connection: NWConnection?
    weak var delegate: ConnectionDelegate?
            
    func startConnection() {
        connection = NWConnection(host: "127.0.0.1", port: NWEndpoint.Port("8000")!, using: .tcp)
        connection?.stateUpdateHandler = { [weak self] newState in
            guard let strongSelf = self else { return }
            switch newState {
              case .setup:
                  print("Connection Setup")
        
              case .preparing:
                  print("Connection Preparing")
        
              case .ready:
                  print("Connection Established")
                  strongSelf.delegate?.connectionDidUpdate(strongSelf, true)
              case .waiting(let error):
                  print("Waiting with \(error)")
        
              case .failed(let error):
                  print("Failed with \(error)")
        
              default:
              break
            }
        }
        connection?.start(queue: .main)
        print("connection start")
    }
          
    func connectionDidUpdate(_ conn: Connection, _ state: Bool) {
        print("Did update state to ready: \(state), \(conn)")
    }
  
    func send(message: String) {
        print("sending '\(message)' while connection state is \(String(describing: connection?.state))")
        connection?.send(content: Data(message.utf8), completion: NWConnection.SendCompletion.contentProcessed { error in
            if error != nil {
                print("Error sending: \(error?.localizedDescription)")
            }
        })
    }
}

On the terminal listen on port 8000 with netcat:

$ nc -l 8000


Now either run your project from Xcode or run the executable from the commandline:

$ ./NetworkCLI


And you should see:

connection start
Connection Preparing
Connection Established
sending 'test message 1' while connection state is Optional(Network.NWConnection.State.ready)
sending 'test message 2' while connection state is Optional(Network.NWConnection.State.ready)
sending 'test message 3' while connection state is Optional(Network.NWConnection.State.ready)


With the listening end contains:

test message 1test message 2test message 3


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com

ahh, yes I see. Yes, I get the correct results when using dispatchMain(). For anyone else, I found a good explanation of dispatchMain() here. It makes most sense if you follow the thread discussion all the way through.


I also realised (as a result of the above) that I was mis-declaring 'conn = Connection()' inside methods in the iOS app. Which obviously meant conn disappeared as soon as the method finished. And that was why it was intermittently working (especially with the 'sleep 10' which was actually meant for the cli version). So I was confusing myself... 😁


Using the above, I've now got the correct results when importing the framework into both an iOS app and the command line tool. Which is exactly what I need.


many thanks for your help/patience.

No problem at all. Glad to help. Yes, Quinn's explanation, in the post that you linked, describing how dispatchMain works is an excellent reference, especially for this context.


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com