Why does webSocketTask.receive never complete and how do I force completion in my Swift app?

372 views Asked by At

I have a Swift app that uses a web socket to download stock price information from a public API. I send a message through the socket to subscribe to various stock price changes then wait for a continuous stream of messages to be received but when I turn off wifi the call to the message receive function, webSocketTask.receive, never returns. How can I force abort of the message receive function so that I alert the user that the network connection has been lost.

Here is my NetworkServices class;

import UIKit
import Network

protocol NetworkServicesDelegate: AnyObject {
    func sendStockInfo(stocksInfo: [String: StockInfo])
}

final class NetworkServices: NSObject {
    
    static let sharedInstance = NetworkServices()
    
    var urlSession: URLSession?
    
    var webSocketTask: URLSessionWebSocketTask?
    
    var stocksInfo: [String: StockInfo] = [:]
    
    var socketResults: [String: [StockInfo]] = [:]
    
    weak var delegate: NetworkServicesDelegate?
    
    var timer: Timer?
    
    var stockSymbols: [String] = []
        
    private let queue = DispatchQueue.global(qos: .background)
    
    private let monitor = NWPathMonitor()
    
    public private(set) var isConnected: Bool = false
    
    public private(set) var connectionType: ConnectionType = .unknown
    
    enum ConnectionType {
        case wifi
        case cellular
        case wiredEthernet
        case unknown
    }
    
    public func startMonitoring() {
        monitor.start(queue: queue)
        monitor.pathUpdateHandler = { path in
            self.isConnected = path.status == .satisfied
            self.getConnectionType(path)
            print("DEBUG: isConnected = \(self.isConnected); connectionType = \(self.connectionType)")
            if self.isConnected == false {
                
            }
        }
    }
    
    public func stopMonitoring() {
        monitor.cancel()
    }
    
    private func getConnectionType(_ path: NWPath) {
        if path.usesInterfaceType(.wifi) {
            connectionType = .wifi
        } else if path.usesInterfaceType(.cellular) {
            connectionType = .cellular
        } else if path.usesInterfaceType(.wiredEthernet) {
            connectionType = .wiredEthernet
        } else {
            connectionType = .unknown
        }
    }
    
    func fetchStockInfo(symbols: [String], delegate: CompanyPriceListVC) {
        
        stockSymbols = symbols
        
        self.delegate = delegate
        
        let configuration = URLSessionConfiguration.default
        
        configuration.waitsForConnectivity = false
                        
        urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
        
        webSocketTask = urlSession?.webSocketTask(with: FINNHUB_SOCKET_STOCK_INFO_URL!)
        
        webSocketTask?.delegate = self
                
        webSocketTask!.resume()
        
        for symbol in symbols {
            
            let string = FINNHUB_SOCKET_MESSAGE_STRING + symbol + "\"}"
            
            let message = URLSessionWebSocketTask.Message.string(string)
            
            if isConnected {
                
                webSocketTask!.send(message) { error in
                    if let error = error {
                        print("DEBUG: Error sending message: \(error)")
                    }
                    
                    self.receiveMessage()
                    
                }
            } else {
                
                // Post notification to view controllers that connection has been lost
                let name = Notification.Name(rawValue: isNotConnectedNotificationKey)
                NotificationCenter.default.post(name: name, object: nil)
                
                // try to re-connect
                // successful re-connect?
                // No, keep trying. Yes, call send message method again
            }
        }
    }
            
    private func receiveMessage() {
        
        if isConnected {
            
            self.webSocketTask?.receive { result in
                
                print("DEBUG: Inside closure.")
                
                switch result {
                
                case .failure(let error):
                    print("DEBUG: Error receiving message: \(error.localizedDescription)")

                case .success(.string(let jsonData)):

                    guard let stockData = jsonData.data(using: .utf8) else { return }
                    
                    self.socketResults = [:]
                    self.stocksInfo = [:]
                    
                    let decoder = JSONDecoder()
                    do {
                        let socketData = try decoder.decode(SocketData.self, from: stockData)
                        guard let stockInfoData = socketData.data else { return }
                        for stockInfo in stockInfoData {
                            let symbol = stockInfo.symbol
                            if self.socketResults[symbol] == nil {
                                self.socketResults[symbol] = [StockInfo]()
                            }
                            self.socketResults[symbol]?.append(stockInfo)
                        }
                        
                        for (symbol, stocks) in self.socketResults {
                            for item in stocks {
                                if self.stocksInfo[symbol] == nil {
                                    self.stocksInfo[symbol] = item
                                } else if item.timestamp > self.stocksInfo[symbol]!.timestamp {
                                    self.stocksInfo[symbol] = item
                                }
                            }
                        }

                        self.delegate?.sendStockInfo(stocksInfo: self.stocksInfo)
                        
                        self.receiveMessage()
                        
                    } catch {
                        print("DEBUG: Error converting JSON: \(error)")
                    }
                    
                default:
                    print("DEBUG: default")
                }
            }
            print("DEBUG: Got here 1")
            
        } else {
            print("DEBUG: Got here 2")
            // Post notification to view controllers that connection has been lost
            let name = Notification.Name(rawValue: isNotConnectedNotificationKey)
            NotificationCenter.default.post(name: name, object: nil)

            // try to re-connect.
            // successful reconnect?
            // No, keep trying. Yes, call receive message method again.
        }
    }

    func closeWebSocketConnection() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {

        if let error = error {
            print("DEBUG: didCompleteWithError called: error = \(error)")
        }
    }
    
    
    func fetchCompanyInfo(symbol: String, completion: @escaping (CompanyInfo?, UIImage?)->()) {
        
        let urlString = FINNHUB_HTTP_COMPANY_INFO_URL_STRING + symbol + "&token=" + FINNHUB_API_TOKEN
        
        guard let url = URL(string: urlString) else { return }
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Error fetching company info: \(error)")
            }
            guard let data = data else { return }
            let decoder = JSONDecoder()
            
            do {
                let companyInfo = try decoder.decode(CompanyInfo.self, from: data)
                
                guard let logoURL = URL(string: companyInfo.logo) else { return }
                
                let task = URLSession.shared.dataTask(with: logoURL) { data, response, error in
                    if let error = error {
                        print("Error fetching logo image: \(error)")
                    }
                    guard let data = data else { return }
                    
                    guard let logoImage = UIImage(data: data) else { return }
                    
                    completion(companyInfo, logoImage)
                    
                }
                task.resume()
            } catch {
                print("Error decoding JSON: \(error)")
                completion(nil, nil)
            }
        }
        task.resume()
    }
}

extension NetworkServices: URLSessionTaskDelegate, URLSessionWebSocketDelegate, URLSessionDelegate {
    
    func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
        print("DEBUG: inside taskIsWaitingForConnectivity")
    }
    
    func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
        print("DEBUG: didBecomeInvalidWithError: error = \(String(describing: error?.localizedDescription))")
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {

        let reasonString: String
        if let reason = reason, let string = String(data: reason, encoding: .utf8) {
            reasonString = string
        } else {
            reasonString = ""
        }

        print("DEBUG: didCloseWith called: close code is \(closeCode), reason is \(String(describing: reasonString))")
    }

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        print("DEBUG: urlSessionDidFinishEvents called")
    }
}

I have tried placing code in the pathUpdateHandler closure to cancel the task on the task object and finish current tasks and cancel on the URLSession object but neither works.

1

There are 1 answers

0
Stephen501 On

I needed to post a notification when path.status == .satisfied. The notification goes to the custom UIViewController class which then calls the FetchStockInfo(symbols:delegate) method.