Search code examples
swiftnetwork-programmingudpbonjourios14

UDP Listener on iOS 14


I have a question regarding how to set up a UDP listener on iOS 14. I have a UDP listener which has worked in the past, but after updating to iOS 14 it works sporadically/not at all.

This lives in an NSObject, and listens for a UDP broadcast across the local network on port 15000 (no specific IP address). It uses the CocoaAsyncSocket library. When I call setUpSocket() local network permissions are not triggered, but the app is able to sporadically pick up UDP packets.


var socket: GCDAsyncUdpSocket?
var broadcastPort: UInt16 = 15000
var broadcastAddress: String = ""
var connectAddress = ""
var connectPort = 0

func setUpSocket() {
    findUDP()
    let socket = GCDAsyncUdpSocket(delegate: self, delegateQueue: DispatchQueue.main)
     
    socket.setIPv4Enabled(true)
    socket.setIPv6Enabled(false)
     
    do {
      try socket.bind(toPort: broadcastPort) /*15000*/
      try socket.enableBroadcast(false)
      try socket.beginReceiving()
       
    } catch let error as NSError {
       
      print("Issue with setting up listener \(error)")
       
    }
     
  }

/*Called when UDP packets are received.*/
func udpSocket(_ sock: GCDAsyncUdpSocket, didReceive data: Data, fromAddress: Data, withFilterContext filterContext: Any?) {
     
    do {
      let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
       
      if (connected == false) {
        if (jsonDictionary["Addresses"] != nil) {
          if (jsonDictionary["Addresses"] is NSArray) {
            let addresses = jsonDictionary["Addresses"] as! NSArray
             
            for i in addresses {
              let ipAddress:String = i as! String
              if (ipAddress.range(of: "^([0-9]{1,3}\\.){3}[0-9]{1,3}(\\/([0-9]|[1-2][0-9]|3[0-2]))?$", options: .regularExpression) != nil) {
                connectAddress = ipAddress
              }
            }
            connectPort = jsonDictionary["Port"] as! Int
          }
           
          /*Sets up a TCP connection on the IP and Port provided in the UDP broadcast.*/
          setupNetworkCommunication(ip: connectAddress, port: connectPort)
          
          closeSocket()

        }
      }
       
    } catch let error {
      return print(error)
    }
  }

How can I update this to comply with iOS 14? If I need to update to use Bonjour services, how can I listen on a port without specifying an address (and without having to look for a specific Bonjour service broadcast, because the broadcast I'm looking for doesn't use Bonjour).

Is it acceptable to quickly open and close a Bonjour NWBrowser in order to trigger the network permissions, and then use my code as-is? This seems to work but seems hacky at best.

Thanks in advance.


Solution

  • I was able to explore this some more and got some help via the apple developer forums, posting an answer here as well for those who are interested.

    I ended up using an NWListener to listen for UDP packets, then set up an NWConnection once once I'd received something. I use this NWConnection to read data from the UDP broadcast.

    From Quinn "The Eskimo:"

    Listening for UDP broadcasts via an NWListener and then using the NWConnection objects it vends (via the new connection handler) to communicate over unicast with the broadcast’s sender is an expected use case.

    I encourage anyone reading this to check out our discussion on the Apple Developer Forum as well.

    Here is my implementation:

      var udpListener: NWListener?
      var udpConnection: NWConnection?
      var backgroundQueueUdpListener  = DispatchQueue.main
       
      func findUDP() {
        let params = NWParameters.udp
        udpListener = try? NWListener(using: params, on: 15000)
         
        udpListener?.service = NWListener.Service.init(type: "_appname._udp")
         
        self.udpListener?.stateUpdateHandler = { update in
          print("update")
          print(update)
          switch update {
          case .failed:
            print("failed")
          default:
            print("default update")
          }
        }
        self.udpListener?.newConnectionHandler = { connection in
          print("connection")
          print(connection)
          self.createConnection(connection: connection)
          self.udpListener?.cancel()
        }
        udpListener?.start(queue: self.backgroundQueueUdpListener)
      }
       
      func createConnection(connection: NWConnection) {
        self.udpConnection = connection
          self.udpConnection?.stateUpdateHandler = { (newState) in
            switch (newState) {
            case .ready:
              print("ready")
              self.send()
              self.receive()
            case .setup:
              print("setup")
            case .cancelled:
              print("cancelled")
            case .preparing:
              print("Preparing")
            default:
              print("waiting or failed")
            }
          }
          self.udpConnection?.start(queue: .global())
      }
       
      func endConnection() {
        self.udpConnection?.cancel()
      }