Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Bonjour Connectivity Optimization

Hi folks, I'm building an iOS companion app to a local hosted server app (hosted on 0.0.0.0). The MacOS app locally connects to this server hosted, and I took the approach of advertising the server using a Daemon and BonjourwithTXT(for port) and then net service to resolve a local name. Unfortunately if there's not enough time given after the iPhone/iPad is plugged in (usb or ethernet), the app will cycle through attempts and disconnects many times before connecting and I'm trying to find a way to only connect when a viable en interface is available.

I've run into a weird thing in which the en interface only becomes seen on the NWMonitor after multiple connection attempts have been made and failed. If I screen for en before connecting it simply never appears. Is there any way to handle this such that my app can intelligently wait for an en connection before trying to connect? Attaching my code although I have tried a few other setups but none has been perfect.

func startMonitoringAndBrowse() {
    DebugLogger.shared.append("Starting Bonjour + Ethernet monitoring")

    if !browserStarted {
        let params = NWParameters.tcp
        params.includePeerToPeer = false
        params.requiredInterfaceType = .wiredEthernet

        browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_mytcpapp._tcp", domain: nil), using: params)
        browser?.stateUpdateHandler = { state in
            if case .ready = state {
                DebugLogger.shared.append("Bonjour browser ready.")
            }
        }
        browser?.browseResultsChangedHandler = { results, _ in
            self.handleBrowseResults(results)
        }

        browser?.start(queue: .main)
        browserStarted = true
    }

    // Start monitoring for wired ethernet
    monitor = NWPathMonitor()
    monitor?.pathUpdateHandler = { path in
        let hasEthernet = path.availableInterfaces.contains { $0.type == .wiredEthernet }
        let ethernetInUse = path.usesInterfaceType(.wiredEthernet)

        DebugLogger.shared.append("""
        NWPathMonitor:
        - Status: \(path.status)
        - Interfaces: \(path.availableInterfaces.map { "\($0.name)[\($0.type)]" }.joined(separator: ", "))
        - Wired Ethernet: \(hasEthernet), In Use: \(ethernetInUse)
        """)

        self.tryToConnectIfReady()
        self.stopMonitoring()
    }
    monitor?.start(queue: monitorQueue)
}

// MARK: - Internal Logic

private func handleBrowseResults(_ results: Set<NWBrowser.Result>) {
    guard !self.isResolving, !self.hasResolvedService else { return }

    for result in results {
        guard case let .bonjour(txtRecord) = result.metadata,
              let portString = txtRecord["actual_port"],
              let actualPort = Int(portString),
              case let .service(name, type, domain, _) = result.endpoint else {
            continue
        }

        DebugLogger.shared.append("Bonjour result — port: \(actualPort)")
        self.resolvedPort = actualPort
        self.isResolving = true
        self.resolveWithNetService(name: name, type: type, domain: domain)
        break
    }
}


private func resolveWithNetService(name: String, type: String, domain: String) {
    let netService = NetService(domain: domain, type: type, name: name)
    netService.delegate = self
    netService.includesPeerToPeer = false
    netService.resolve(withTimeout: 5.0)
    resolvingNetService = netService
    DebugLogger.shared.append("Resolving NetService: \(name).\(type)\(domain)")
}

private func tryToConnectIfReady() {
    guard hasResolvedService,
          let host = resolvedHost, let port = resolvedPort else { return }

    DebugLogger.shared.append("Attempting to connect: \(host):\(port)")
    discoveredIP = host
    discoveredPort = port
    connectionPublisher.send(.connecting(ip: host, port: port))
    stopBrowsing()
    socketManager.connectToServer(ip: host, port: port)
    hasResolvedService = false
}

}

// MARK: - NetServiceDelegate extension BonjourManager: NetServiceDelegate { func netServiceDidResolveAddress(_ sender: NetService) { guard let hostname = sender.hostName else { DebugLogger.shared.append("Resolved service with no hostname") return }

    DebugLogger.shared.append("Resolved NetService hostname: \(hostname)")
    resolvedHost = hostname
    isResolving = false
    hasResolvedService = true
    tryToConnectIfReady()
}

func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
    DebugLogger.shared.append("NetService failed to resolve: \(errorDict)")
}

}

I'm building an iOS companion app to a local hosted server app (hosted on 0.0.0.0). The MacOS app locally connects to this server hosted

I’d like to clarify your setup here:

  • Your server is running on macOS, right?

  • From the above it sounds like you have both iOS and macOS client apps. Is that right?

Unfortunately if there's not enough time given after the iPhone/iPad is plugged in (usb or ethernet)

Can you expand on the physical topology here? In most situations like this the Mac and the iPhone are on the same infrastructure Wi-Fi, and thus there’s no plugging involved.

Finally, you seem to be mixing Network framework and NetService. Why is that? Most folks who use NWBrowser don’t need to resolve the resulting endpoint because they can pass it directly to NWConnection. And, critically, NWConnection will wait for connectivity.

Share and Enjoy

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

Hi Quinn,

Yes, there is a macOS client that just connects locally, and this is the iOS companion.

This is really an app im designing for mainly the iPad to be used over ethernet, as the server is a DAW extension app (so like on stage). So I have a manual mode that has always worked fine, but Ive been trying to get a zeroconf working. I've been testing usb as well.

I've certainly tried to use the result.endpoint, but it leaves me stuck in a .preparing state.

Essentially, the server is a local python server running on 0.0.0.0. and is advertised through a swift daemon running NWListener. My (albeit limited) understanding was that since the nil domain is .local that, as long as the port was being correctly read from txt (which it is), that the endpoint should work. but since it wasn't I tried to resolve the hostname which does work but just with the aforementioned issues.

Using NWConnection to connect to the endpoint returned by NWBrowser should work.

I’d still like to clarify the network topology here. You wrote:

This is really an app im designing for mainly the iPad to be used over ethernet

which is unusual, but certainly should work. So how is this set up?

Here’s my speculation on that point, rendered in the finest ASCII art (-:

                                              The Internet™
                                                    |
Ethernet ----+-----------+------------+-------------+----
             |           |            |
        Mac server   Mac client   iPad client

Is that accurate? And, if so, what’s the disposition of Wi-F on each of these devices? Is it off? Or associated with a Wi-Fi network? And, if so, does that also lead to the Internet? And is that bridged to your Ethernet?

Share and Enjoy

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

I think that's accurate. It would mostly be used completely disconnected from wifi on all devices, but shouldn't necessarily need it to be turned off to work.

That's why I use this in my code, to ensure the only connections accepted are via wired ethernet:

let parameters = NWParameters.tcp
        parameters.requiredInterfaceType = .wiredEthernet  // force Ethernet-only

        let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ip), port: NWEndpoint.Port(rawValue: UInt16(port))!)
        let connection = NWConnection(to: endpoint, using: parameters)
        self.connection = connection

of course, when I try to use result.endpoint directly I just pass the NWEndpoint in as the parameter directly but this snippet is taken from my net service implementation.

Were it not for my desire for zeroconf, manual connection via linked local would be just fine. But I don't want to lean on internet sharing, which is why I used Bonjour. I hope that gives you enough info regarding my topology?

There are a bunch of potential paths between your iPad client and your Mac:

  • Wi-Fi

  • Peer-to-peer Wi-Fi

  • Ethernet

  • The tunnel used by Xcode’s debugging infrastructure

I recommend that you start ruling them out. To eliminate the Wi-Fi cases, turn Wi-Fi off.

IMPORTANT Don’t turn it off from the Control Centre, which doesn’t really turn it off. Rather, turn it off in Settings.

Oh, and I’m not recommending this as the solution that you deploy, just something to rule out Wi-Fi while you investigate this issue.

As to the difference between Ethernet and the Xcode tunnel, setting up the browser to work on a specific interface type should prevent it from returning the latter, but let’s confirm that. When you get the NWEndpoint, what do you see for the interface associated value?

Share and Enjoy

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

Hi Quinn. Wifi has been off, peer to peer is disabled in the code in a few places. The issue I was having that in my socket connection .preparing state my code

                case .preparing:
                    let path = connection.currentPath
                    DebugLogger.shared.append("Preparing with path: \(String(describing: path))")
                    if let endpoint = path?.remoteEndpoint {
                        DebugLogger.shared.append("Target endpoint: \(endpoint)")
                    }
                    if let interfaces = path?.availableInterfaces {
                        var counter = 0
                        for intf in interfaces {
                            DebugLogger.shared.append("🔌 Interface: \(intf.name), type: \(intf.type)")
                            if case intf.type = .wiredEthernet {
                                counter += 1
                            }
                        }
                        if counter < 0 {
                            self.connection?.cancel()
                        }
                    } else {
                        self.connection?.cancel()
                    }

would produce multiple 'path is satisfied', but no valid wired ethernet interface, and continuously fail until it reached a valid one (en1 on my usb and en5 on my ethernet adapter that goes direct from my MBP to my iPhone).

Interestingly I had my data roaming on as im abroad and having that enabled caused a Bridge100 ipV4 to correctly connect, but I cant rely on that being enabled for an end user, and regardless it doesn't make a difference for my ethernet-only connection.

I've attached some photos of the debugger essentially cycling until en requirement is satisfied. If I try to enforce wired ethernet requirement at the NWMonitor level it simply stalls in a [.cellular] only state. and once again, using a result.endpoint in this same environment (no data roaming, no wifi, no peer to peer etc) it stalls in a .preparing state.

Just wanted to quickly follow up here. Is the general assessment here just that one should not use Bonjour if they cant host an NWListener in the same application as their server is hosted?

I made a dummy NWListener in the typical way to test connecting to a service and as expected it worked perfectly in my ecosystem. The struggle to quickly discover the .wiredEthernet interface is only for my hostPort style NWConnection.

Is the general assessment here just that one should not use Bonjour if they cant host an NWListener in the same application as their server is hosted?

No. I’m still not really sure I understand what’s going on here, but Network framework should be able to deal with these scenarios.

The struggle to quickly discover the .wiredEthernet interface is only for my .hostPort(…) style NWConnection.

I’m still not sure why you’ve involved .hostPort(…) at all. In general, you want to stay away from that when using Bonjour, and instead use a .service(…) endpoint and let the connection deal with resolving that.

Specifically, code like this is problematic:

parameters.requiredInterfaceType = .wiredEthernet  // force Ethernet-only

let endpoint = NWEndpoint.hostPort(host: NWEndpoint.Host(ip), port: NWEndpoint.Port(rawValue: UInt16(port))!)

The issue is that IP addresses are scoped to a particular interface. That’s true for IPv4 but it’s particularly important for IPv6. If you force a connection to run over a specific interface and the endpoint isn’t appropriate for that interface, things will just fail.

So, I gotta ask: Why are you resolving the service to an IP?

If there’s some compelling reason you need to do that, it’s still better to avoid IP addresses. A better option is to construct a .hostPort(…) endpoint from the resolved host name (resolvedHost in your initial post). That’ll give NWConnection access to the full suite of IP addresses, which should improve your chances of connection.

FWIW, I actually set up my Mac and iPad on Ethernet today so I could test your scenario. I didn’t see any oddities. The iPad was always able to connect with the .wiredEthernet constraint, even when I had Wi-Fi enabled.

Share and Enjoy

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

Hi Quinn,

I didn't show it exactly but I exactly was passing my resolved hostname as the parameter for that 'ip'. I really just called it ip because I use the same function for my 'manual' connection option. you can see in those debug log screenshots above I think it reflects that its using the hostname in its attempted connection.

It totally would work, but I think given that it has a live music application I just wanted it to connect faster. if it got unplugged during a show I wouldn't want it to take so much time reconnecting while it sniffs out the scoped interface.

I've since changed my architecture such that the local daemon is itself the server client and the iOS app can connect directly to the daemon as a .service endpoint and it is so much faster so I think im happy with this solution.

The one thing that I feel like im missing in this scenario is how I would have been able to do that previous approach without possibly resolving. If your server is written in another language (in an implementation that has no access to zeroconf libraries) and you use this additional daemon as a way to advertise a path to that local server(put intended port number in txtRecord), resolving a hostname seems like the only way to me aside from changing the architecture such as I have. Thank you for your help on this!

Glad to hear you’re making progress.

If your server is written in another language (in an implementation that has no access to zeroconf libraries)

Honestly, I don’t spend a lot of time worrying about that case because my experience is that the vast majority of platforms and languages have support for Bonjour [1].

But as to how you deal with this without Bonjour, there are a bunch of common options:

  • Implement your own service discovery. This is a bad idea IMO, but lots of people do it [2].

  • Force the user to type in IP addresses and ports. Also a bad idea, but again an ‘industry standard’ practice.

  • Use a Bonjour proxy. You can implement this in code or use the one built in to macOS (run dns-sd with the -P option).

  • Use a lower-level API. For example, the stuff you’re doing with NetService can be done with the DNS-SD API. I put both variants of that code into this thread, here and here.

Share and Enjoy

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

[1] Well, technically Bonjour is an Apple term, so I’m referring to the three industry-standard protocols that it uses:

[2] It’s one of the main ‘caller drivers’ for broadcast questions here on the forums. If you’re curious, my advice on that topic is in Extra-ordinary Networking > Broadcasts and Multicasts, Hints and Tips.

Bonjour Connectivity Optimization
 
 
Q