0

I'm quite new to multi-threading in iOS, and have some difficulties with the implementation of my task. I'd really appreciate any help from more experienced programmers.

I have one URL address, as a starting point, to which I make a request, check some info, and retrieve all the URL addresses contained on that web-page. After this, I need to do the same with new URL addresses again and again, until a specific number of URLs is checked.

I decided to go with Operations and OperationQueues, because I also need to be able to choose a maximum number of concurrent operations.

I created a custom async operation LoadOperation and applied it to each URL stored in an array. In the completion block, I add new URLs to the array, and call the same function, kind of recursively.

Also I created two queues:

  • A concurrent one for requests,

and

  • A serial one for accessing shared properties as follows:
class Queue<Item: Equatable> {

    // concurrent queue for URL-requests
    let loadOperationQueue = OperationQueue() // qos: .utilities

    // serial operation queue for accessing shared properties 
    let serialOperationQueue = OperationQueue() // qos: .utilities

    // maximum number of URLs that need to be checked ultimately
    var maxNumberOfItems: Int = 1

    var pendingItems: [Item] = []   // newly added URLs
    var loadingItems: [Item] = []   // loading URLs
    var processedItems: [Item] = [] // URLs that were checked

    // all URLs for tableView dataSource
    var allItems: [Item] {
        return processedItems + loadingItems + pendingItems
    }

    func addToPending(_ item: Item) {
        guard allItems.count < maxNumberOfItems else {return}
        pendingItems.append(item)
    }

    func removeFromPending() -> Item? {
        guard !self.pendingItems.isEmpty else { return nil }
        let item = pendingItems.removeFirst()
        loadingItems.append(item)
        return item
    }

    func addToProcessed(_ item: Item) {
        loadingItems = loadingItems.filter{$0 != item}
        processedItems.append(item)

    }

    func isFull() -> Bool {
        return allItems.count >= maxNumberOfItems
    }
}

This is my searching function:

    func startSearching() {

        // iterate over array of URLs creating async operation
        while let currentWebPage = queue.removeFromPending() {

            let loadOperation = LoadOperation(currentWebPage, searchString: searchString)

            loadOperation.completionBlock = {
                let webPage = loadOperation.output!

                self.queue.serialOperationQueue.addOperation {

                    // add checked URL to processed
                    self.queue.addToProcessed(webPage)

                    // add new urls to an array of pending URLs
                    for urlString in webPage.containedLinks {
                        //check if the limit of checked urls is not exceeded
                        guard !self.queue.isFull() else { return }
                        //check if this url is already in queue
                        guard !self.queue.allItems.contains(where: {$0.url == urlString}) else { continue }

                        self.queue.addToPending(WebPage(url: urlString, containedLinks: [], status: .pending))
                    }

                    // update UI
                    OperationQueue.main.addOperation {
                        self.viewDelegate?.reloadTable()
                    }

                    // repeat searching process with new urls
                    self.startSearching()
                }
            }
            queue.loadOperationQueue.addOperation(loadOperation)
        }

    }

I can't figure out why:

  1. When I run the app, the screen freezes. Even though all my queues are in the background (qos: utilities).

  2. Sometimes when I try to scroll UITableView, I get sigabort due to index out of range, even though I try to access all properties in serial queue.

This is a data source code:

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return presenter.getNumberOfRows()
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "WebPageTableViewCell", for: indexPath) as! WebPageTableViewCell
        let (url, status) = presenter.getCellContent(at: indexPath.row)
        cell.addressLabel.text = url
        cell.statusLabel.text = status
        return cell
    }

And functions from presenter:

    func getNumberOfRows() -> Int {
        return queue.allItems.count
    }

    func getCellContent(at index: Int) -> (url: String, status: String) {
        return (url: queue.allItems[index].url, status: queue.allItems[index].status.description)
    }
Alina Vas
  • 5
  • 5
  • You are reloading the table each time (which is a bit excessive, it is better just insert the additional row) and you are getting array range crashes, presumably because of concurrent access to the processedItems array; show your tableview datasource code since that is probably where your issues are; that is the code executing on the main queue – Paulw11 Apr 09 '19 at 20:32
  • @Paulw11, thank you for your response! 1. Would it be better to insert rows separately even if I have a whole bunch of items (URLs) to add? 2. I thought that I prevented concurrent accessing of data source arrays on a custom serial queue, so that just one operation at a time could make changes to it. Am I wrong? I'll add data source code in a question. – Alina Vas Apr 10 '19 at 11:45

0 Answers0