Activity Indicator is not animated when press navigationItem button

198 Views Asked by At

I try to trigger activity indicator's animation when press navigationItem's button. But I found the activity indicator is not spinning. And I try to put scanerIndicator.startAnimating() to main thread, however no help.

The code is collected the opened port of router, I want to start the spinning when press navigationItem button and stop the spinning when openPorts was returned. Appreciate for any clue/hint about where is wrong?

    override func viewDidLoad() {
        super.viewDidLoad()
        ...
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Start", style: .plain, target: self, action: #selector(startScan))
        ...
    }

    @objc func startScan() {
        scanerIndicator.startAnimating()
        
        if let address = serverAddress.text, !address.isEmpty {
            if let start = Int(startPort.text!) {
                if let stop = Int(stopPort.text!) {
                    if start < stop {
                        openPorts = netUtility.scanPorts(address: address, start: start, stop: stop)
                        print("Open Open: \(openPorts)")
                        if !openPorts.isEmpty {
                            scanerIndicator.stopAnimating()
                            table.reloadData()
                        } else {
                            showErrorMessage(errorTitle: "Not at all", errorMessage: "No open ports were found")
                        }
                    } else {
                        showErrorMessage(errorTitle: "Range error", errorMessage: "Start port should be smaller than stop port")
                    }
                } else {
                    showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
                }
            } else {
                showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
            }
        } else {
            showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
        }
    }

Code to collect the ports:

   // MARK: - Port Scaner
    // Get number of threads for scan ports
    func getSegmentsQueues(min: Int, max: Int, maxPerSegment: Int) -> [[Int]] {
        
        var start: Int = min
        var portSegments = [[Int]]()
        
        while start <= max {
            var _portSegment = [Int]()
            
            for _ in 1...maxPerSegment {
                
                if start <= max {
                    _portSegment.append(start)
                }
                
                start += 1
            }
            
            portSegments.append(_portSegment)
        }
        
        return portSegments
    }


    // Crate queques for scan ports by segments
    func QueueDispatchPort(address: String, minPort: Int, maxPort: Int, segmentsQueues: (Int, Int, Int) -> [[Int]]) -> [Int] {
        var openPorts : [Int] = []
        let segmentPorts = segmentsQueues(minPort, maxPort, 1);
        
        let group = DispatchGroup()
        
        for segment in segmentPorts {
            group.enter()
            DispatchQueue.global().async {
                
                for port in segment {
                    let client = TCPClient(address: address, port: Int32(port))
                    switch client.connect(timeout: 2) {
                        case .success:
                            openPorts.append(port)
                        
                        case .failure(_):
                            print("port \(port) closed")
                    }
                    
                    client.close()
                }
                group.leave()
            }
        }
        
        group.wait()

        return openPorts
    }
    
    // Scans ports from an address and a range given by the user
    func scanPorts(address : String, start : Int, stop : Int) -> [Int] {
        let openPorts = QueueDispatchPort(
            address: address, minPort: start, maxPort: stop, segmentsQueues:
            getSegmentsQueues(min:max:maxPerSegment:))
        
        return openPorts
    }

Code update, I put the chunk of code(scan port) on main thread, and remove stopAnimating() for this time. The activityIndicator is animated after long-run code return(what in DispatchQueue.main). Still not work...

@objc func startScan() {
        scanerIndicator.startAnimating()
        
        DispatchQueue.main.async { [self] in
            if let address = serverAddress.text, !address.isEmpty {
                if let start = Int(startPort.text!) {
                    if let stop = Int(stopPort.text!) {
                        if start < stop {
                            openPorts = netUtility.scanPorts(address: address, start: start, stop: stop)
                            print("Open Open: \(openPorts)")
                            if !openPorts.isEmpty {
                                table.reloadData()
                            } else {
                                showErrorMessage(errorTitle: "Not at all", errorMessage: "No open ports were found")
                            }
                        } else {
                            showErrorMessage(errorTitle: "Range error", errorMessage: "Start port should be smaller than stop port")
                        }
                    } else {
                        showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
                    }
                } else {
                    showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
                }
            } else {
                showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
            }
        }
    }
2

There are 2 best solutions below

4
Duncan C On BEST ANSWER

Its a little hard to understand what your code is doing, but my guess is that even though you're using queues to do your port scanning, because you are using a DispatchGroup, the code blocks until all the port scanning is complete.

If you have synchronous code that does the following:

  1. Start animating activity indicator
  2. Do long-running task (on main thread)
  3. Stop animating activity indicator

Then you never see the animation. The problem is that the animation doesn't start until your code returns and your app visits its event loop.

You instead need to write your code like this:

scanerIndicator.startAnimating()
DispatchQueue.main.async {
   //Do long-running task
   scanerIndicator.stopAnimating()
}

That works because after calling startAnimating(), you add an async call to the main dispatch queue (on the main thread), then return. Your app's function call stack all returns, your app visits the event loop, and the activity indicator begins spinning. The system then picks up the async task you added to the main queue and begins running that task. Finally, when your long-running task is complete, you turn off the activity indicator (In the code that's called inside the async() call.

0
Zhou Haibo On

Finally, finally I make this code works, DispatchQueue really burn my brain the whole day!

My fixing flow:

    func abc() {
        activityIndicator.startAnimating()
        
        DispatchQueue.global(qos: .default).async {
            // put your heavy code here
            
            DispatchQueue.main.async {
                // UI code must be on main thread
                activityIndicator.stopAnimating()
            }
        }
    }

What I do?

  1. First, I combine the two port scanning code as 1 function.
  2. Put the long-time consuming code in DispatchQueue.global(qos: .default).async. I try to use main thread firstly, but it failed, also the long-running code is supposed to be in background thread. Is global() is background thread? I will dig more later:)
  3. Use a @escaping completionHandler as callback to give the openPorts to the caller when this heavy code is complete. Because you cannot use return values in this situation, as we don't know when the heavy code is finished, so instead we use callback passing the parameter back to the caller.
    // Scans ports from an address and a range given by the user
    func scanPorts(address : String, start : Int, stop : Int, completion: @escaping ([Int]) -> ()) {
        
        DispatchQueue.global(qos: .default).async {
            for port in start...stop {
                let client = TCPClient(address: address, port: Int32(port))
                switch client.connect(timeout: 2) {
                    case .success:
                        self.openPorts.append(port)
                        print("HH: port: \(self.openPorts)")
                    case .failure(_):
                        print("port \(port) closed")
                }
                client.close()
            }
            completion(self.openPorts)
        }
    }

The caller: just remember to put UI code on main thread here.

@objc func startScan() {
        scanerIndicator.startAnimating()
        view.endEditing(true)
        self.view.isUserInteractionEnabled = false
        
        if let address = serverAddress.text, !address.isEmpty {
            if let start = Int(startPort.text!) {
                if let stop = Int(stopPort.text!) {
                    if start < stop {
                        netUtility.scanPorts(address: address, start: start, stop: stop) { [self] (availablePorts) in
                            openPorts = availablePorts
                            print("$Open ports: \(self.openPorts)")
                            if !openPorts.isEmpty {
                                DispatchQueue.main.async {
                                    table.reloadData()
                                    self.scanerIndicator.stopAnimating()
                                }
                            } else {
                                showErrorMessage(errorTitle: "Not at all", errorMessage: "No open ports were found")
                            }
                        }
                    } else {
                        showErrorMessage(errorTitle: "Range error", errorMessage: "Start port should be smaller than stop port")
                    }
                } else {
                    showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
                }
            } else {
                showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
            }
        } else {
            showErrorMessage(errorTitle: "Empty fields", errorMessage: "Please fill all the necessary data")
        }
    }