2

I am on Swift 4. The goal is to load all the data in an address book, before render the address book in view. In a different language such as js, I may use await in each item in the loop, before telling the view to render the rows. I am looking for the canonical way to solve this issue in Swift 4 with UITableViewController.

Right now the address book is stored in backend with Amplify and GraphQL. I have a User model of form

type User @Model {
  id: ID!
  name: String!
  bio : String!
}

and Contact of form

type Contact @model {
  ownerId: ID!
  userId: ID!
  lastOpened: String
}

In ContactController: UITableViewController.viewDidLoad I fetch all Contact in database where the ownerId is my user's id-token, I then create an object using this contact information. And then for each Contact object instance, I get its corresponding User in database when the object is initialized. Per this post: Wait until swift for loop with asynchronous network requests finishes executing, I am using Dispatch group, and then reload the UITableView after the loop completes and the Dispatch group has ended. But when I print to console, I see that the loop completes before the Contact object has loaded its User information.

Code snippets:

class ContactsController: UITableViewController, UISearchResultsUpdating {

        var dataSource : [Contact] = []


        override func viewDidLoad() {

            super.viewDidLoad()

            let fetchContactGrp = DispatchGroup()

            fetchContactGrp.enter()

            self.getMyContacts(){ contacts in

                for contact in contacts {

                    let _contactData = Contact(
                          userId     : contact.userId
                        , contactId  : contact.id
                        , timeStamp  : contact.timeStamp
                        , lastOpened : contact.lastOpened
                        , haveAccount: true
                    )

                    _contactData.loadData()
                    self.dataSource.append(_contactData)
                }

            }


            fetchContactGrp.leave()

            DispatchQueue.main.async{
                 self.tableView.reloadData()
            }
        }

    }

The function self.getMyContacts is just a standard GraphQL query:

func getMyContacts( callBack: @escaping ([Contact]) -> Void ){

    let my_token = AWSMobileClient.default().username
    let contact = Contact.keys
    let predicate = contact.ownerId == my_token! 

    _ = Amplify.API.query(from: Contact.self, where: predicate) { (event) in
        switch event {
            case .completed(let result):
                switch result {
                    case .success(let cts):
                        /// @On success, output a user list
                        callBack(cts)
                    case .failure(let error):
                        break
                }
            case .failed(let error):
                break
            default:
                break
        }
    }
}

And the Contact object loads the User data from database:

class Contact {

        let userId: String!
        let contactId: String!

        var name : String
        var bio  : String
        var website: String

        let timeStamp: String
        let lastOpened: String


        init( userId: String, contactId: String, timeStamp: String, lastOpened: String, haveAccount: Bool){

            self.userId     = userId
            self.contactId  = contactId
            self.timeStamp  = timeStamp
            self.lastOpened = lastOpened
            self.haveAccount = haveAccount


            self.name = ""
            self.bio  = ""
            self.website = ""

        }

        func loadData(){

            /// @use: fetch user data from db and populate field on initation
            let _ = Amplify.API.query(from: User.self, byId: self.userId) { (event) in

                switch event {
                    case .completed(let res):
                        switch res{
                            case .success (let musr):
                                if (musr != nil){

                                    let userData = musr!
                                    let em    = genEmptyString()
                                    self.name = (userData.name == em) ? "" : userData.name
                                    self.bio  = (userData.bio == em)  ? "" : userData.bio
                                    self.website = (userData.website == em) ? "" : userData.website

                                    print(">> amplify.query: \(self.name)")

                                } else {
                                    break
                                }
                            default:
                               break
                        }
                    default:
                        print("failed")
                }
            }
        }


    }
xiaolingxiao
  • 4,793
  • 5
  • 41
  • 88
  • The way you are using `DispatchGroup` is wrong. You have to add a completion handler to `loadData()`, then call `enter()` inside the loop in `viewDidLoad ` and leave in the completion handler of `loadData()`. Add `group.notify` and reload the table view in the closure. – vadian Apr 25 '20 at 17:15

2 Answers2

0

It's because the function getMyContacts() is performing an Async task and the control goes over that and execute the leave statement. You need to call the leave statement inside the getMyContacts() function outside the for loop.

Try the following code:

override func viewDidLoad() {

            super.viewDidLoad()

            let fetchContactGrp = DispatchGroup()

            fetchContactGrp.enter()

            self.getMyContacts(){ contacts in

                for contact in contacts {

                    let _contactData = Contact(
                          userId     : contact.userId
                        , contactId  : contact.id
                        , timeStamp  : contact.timeStamp
                        , lastOpened : contact.lastOpened
                        , haveAccount: true
                    )

                    _contactData.loadData()
                    self.dataSource.append(_contactData)
                }
                fetchContactGrp.leave()
            }


            fetchContactGrp.wait()

            DispatchQueue.main.async{
                 self.tableView.reloadData()
            }
        }
Hobbit
  • 601
  • 1
  • 9
  • 22
  • 1
    Don't **wait**, there is no reason to do that. And the `DispatchGroup` syntax is as wrong as in the question. – vadian Apr 25 '20 at 17:12
  • @vadian could you provide an alternate solution? I used this example: https://stackoverflow.com/questions/35906568/wait-until-swift-for-loop-with-asynchronous-network-requests-finishes-executing – xiaolingxiao Apr 25 '20 at 18:17
  • Please read my comment to the question. `DispatchGroup` is only useful is `leave()` is called in a completion handler of an asynchronous function. Both the question and the answer ignore the asynchronous behavior of `loadData()` – vadian Apr 25 '20 at 18:19
  • @UmerFaroq I get he same behavior as before with `wait`. Just to be clear, when I `print` the exection steps, it enter, wait and leave the dispatch group, *before* initializting `Contact` and loading the user data. @vadian ok I sort of get what you're sayiing now. Could you provide a code smple please? – xiaolingxiao Apr 25 '20 at 18:31
  • Once again, you have to add a completion handler in `loadData` like in `getMyContacts`. – vadian Apr 25 '20 at 18:36
  • @vadian still learning the basics, could you provide an example of the completion hander I would need? I get concurency in principle, but its execution in Swift is lost to be, as is how to use the API – xiaolingxiao Apr 25 '20 at 18:59
0

I posted a more general version of this question here: Using `DispatchGroup` or some concurency construct to load data and populate cells in `UITableViewController` sequentially

And it has been resolved.

xiaolingxiao
  • 4,793
  • 5
  • 41
  • 88