Search code examples
iosswiftdispatchgroup

Using `DispatchGroup` or some concurency construct to load data and populate cells in `UITableViewController` sequentially


Platform:

I am on swift 4 and xcode 11.4

Use case and desired behavior

The app is loading a feed with potentially 100s or 1000s of items, let's say 500 items. The 500 items will be grabbed once using Amplify's GraphQL query, then each item will then load additional data. The data will populate the cells in a UITableViewController. Ideally this process would happen in the following exact sequence:

  1. query 500 items
  2. cell_1 load additional data.
  3. cell_1 render data and display in UITableViewController
  4. cell_2 load additional data.
  5. cell_2 render data and display in UITableViewController

...

  1. cell_500 load additional data
  2. cell_500 render data and display in UITableViewController

So the user will see a "waterfall" of cells being rendered in the feed.

Question

This seems like a use case that require some finer control of execution, which would need this: https://developer.apple.com/documentation/dispatch/dispatchgroup

I am new to Swift so this is a bit advanced for me. Provided is the stub for GraphQL query, and class function that loads additional data, and the top level UITableViewController. Please instruct how I would use DispatchGroup.

class Feed: UITableViewController {
    
    var dataSource: [FullItem] = []

    override func viewDidLoad(){
       super.viewDidLoad()
       
       queryItem{ items
           
           for item in items {
              let itemInstanceWithMoreData = FullItem( id: item.id )
              itemInstanceWithMoreData.loadFullData()
           }
         
       }           
    }
}


func queryItems( callBack: @escaping ([Item]) -> Void ){

    _ = Amplify.API.query(from: Item.self, where: predicate) { (event) in
        switch event {
            case .completed(let result):
                switch result {
                    case .success(let xs):
                        callBack(xs)
                    case .failure: 
                        break
                }
            case .failed: 
                break
            default:
                break
        }
    }
}


class FullItem {
    
    id: String
    name: String?
    
    init( id ){ self.id = id; self.name = "" }

    
    func loadData(){

        let _ = Amplify.API.query(from: FullItem.self, byId: self.id) { (event) in
            
            switch event {
                case .completed(let res):
                    switch res{
                        case .success (let musr):
                            if (musr != nil){
                                self.name = musr!.name
                            } else {
                                break
                            }
                        default:
                           break
                    }
                default:
                    print("failed")
            }
        }
    }
}

addendum

If the sequence I am asking is not possible, I would also settle for query 500 items, load additional data for each one, then rendering the cells. But either way, the cell should not render with empty data.


Solution

  • Your example is incomplete and doesn't compile so this is the short version

    Declare loadData()

    func loadData(completion: @escaping () -> Void) {
    

    and make sure that completion() is called in any case (this is crucial!) for example

    default:
        completion()
        print("failed")
    

    To use DispatchGroup properly you have to call enter inside the loop before calling the asynchronous task and call leave in the completion handler of the task. At the end outside the loop implement notify

    override func viewDidLoad(){
        super.viewDidLoad()
    
        let group = DispatchGroup()
    
        queryItems { items
    
            for item in items {
                group.enter()
                let itemInstanceWithMoreData = FullItem( id: item.id )
                itemInstanceWithMoreData.loadData {
                    group.leave()
                }
            }
    
            group.notify(queue: .main) {
                self.tableView.reloadData()
            }
    
        }
    }
    

    To insert and update the items serially in order you need an asynchronous Operation and a serial OperationQueue. DispatchGroup doesn't preserve the order.