-1

Swift 3/Xcode 8/ios 10

I have a method that fetches a bunch of data from a few api endpoints. I pick out the bits I want to keep and then format and print that data to a text file.

When the app starts up, it checks to see whether or not the text file is empty. If it is empty, then the API requests are run and the text file is populated accordingly. So far so good. This all works as expected.

I now want to show the user the progress of the data being retrieved via a progress bar. This is where I am getting stuck. I cannot seem to get the progress bar to display before and during the data retrieval. It only displays after data retrieval is complete - and indicates as much too. Via some print statements in the protocol method, I can see that the progress bar is seemingly being updated - it's just that the segue to the progress bar does not happen when I want it to.

There are 3 classes involved in the above description:

  1. MasterVC - checks text file and segues to ProgressBar if necessary. Initiates API calls via GetData.start(), immediately after calling performSegue to ProgressBar.
  2. ProgressBar - shows the progress bar and receives updates from GetData.
  3. GetData - contains the method that fetches data from APIs.

In viewDidAppear of MasterVC, the code below shows the first block of code:

    if fileSize == 0{

        //SHOW PROGRESS OVERLAY AND RETRIEVE API DATA
        performSegue(withIdentifier: "toProgressBarOverlay", sender: self)

        GetData.start() //IF THIS IS ACTIVE, DATA IS RETRIEVED BUT PROGRESS BAR APPEARS AT THE END.

    }

Any code after this runs only once the text file has been populated via the API calls. I have done this via the use of semaphores contained within the API calling method. If instead, I place GetData.start() in viewDidAppear of ProgressBar, I get errors in this section relating to variables/arrays etc being empty.

From googling and searching SO, I get the impression that the late showing of the progress bar has to do with the updating of views only being done at the end of each cycle and that although I call the performSegue first, it only happens last, after the rest of the code in the class has been iterated through.

How can I make this progress bar appear as intended?

EDIT 1:

OK, so I have managed to get the progress bar to now appear at the start of the API calls. Sadly however, the progress bar is not updating properly. The updates are, however, still being reported correctly in the console. The progress bar appears with the default start value, then once the API calling is done, the progress bar suddenly shoots to 100% with no intermediate values. Using dispatchQueue.main.async/sync still does not work. Below are the changes I have made:

The code section above (which is in MasterVC) now looks like the following:

//FETCH MP INFO AND POPULATE MPDATA.TXT.
    if fileSize == 0{

        //SHOW PROGRESS OVERLAY AND RETRIEVE API DATA
        performSegue(withIdentifier: "toProgressBarOverlay", sender: self)

    }

All the code after this (which dealt with the populated text file) has been moved to its own method (called processTextFile).

So when the app starts, it checks for an empty file, and if it finds as much, segues to ProgressBar. ProgressBar also now contains a new reference to MasterVC (called masterVcReference), which is set during prepareForSegue.

ProgressBar now initiates the API calls with:

GetData.start(masterVc: masterVcReference!)

Once all the data has been retrieved and written to file, GetData uses masterVcReference to call processTextFile.

The actual protocol method is as follows:

func updateProgressBar(message: String, percentComplete: Float) {

    print("Progress: \(percentComplete)%") << this is being reported correctly.

    ProgressBar?.setProgress(percentComplete, animated: true)
    ProgressBarMessageField.text = message

    if percentComplete == 100{
        //dismiss(animated: true, completion: nil)
    }
}

If I place "ProgressBar?.setProgress(percentComplete, animated: true)" and "ProgressBarMessageField.text = message" in a dispatchQueue.main.async/sync block, no difference is made.

So currently, the app works as it should except for the progress bar not updating as it should.

EDIT 2:

OK, so using DispatchQueue.global(qos: .userInteractive).async, instead of dispatchQueue.main.async to update the progress bar has made a small difference. The progress bar updates, sort of. Of the 649 updates (from the 649 items of data that are processed) that the progress bar should go through, it updates only 1, 2 or maybe 3 times if I'm lucky. Also, the update frequency is not the same between each run of the app.

EDIT 3:

OK - turns out dispatchQueue.main.async has been working, but it seems to be queueing up all of the updates and executing everything right at the end instead of executing with each protocol method call. Confusion reigns.

Solved at Last!

I was using semaphores to ensure that data from the API calls were processed in the correct order. The solution was to place the "semaphore.signal()" statement BEFORE the call to the delegate method. Originally, the delegate call lay between "let semaphore = DispatchSemaphore(value: 0)" and "semaphore.signal()". Somehow (I don't understand it yet), this was causing the UI updates to queue up and not execute until all of the API data had finished processing.

Voila!

Rossco
  • 37
  • 5
  • Are you *sure* `GetData.start()` is communicating with `ProgressBar`? Can you print() the calls? If the calls *are* being made, then it sounds a lot like your `GetData` class is trying to update the ProgressBar on a background thread, and you need to make sure the call is being made on the `main` thread. – DonMag Mar 27 '17 at 23:10
  • The protocol method is definitely being called - I have a print statement in it that prints to console the % complete. I have also tried placing both the protocol method call as well as the content of the protocol method within dispatchQueue.main.sync/async to no avail. – Rossco Mar 28 '17 at 08:30
  • Why the downvote? My research is stated in the second last paragraph. the vast majority of progress bar related articles/posts either do not apply to my case or involve the use of Alamofire with which am not yet familiar and do not yet use. What is unclear exactly? – Rossco Mar 28 '17 at 08:44
  • (No idea why somebody would down vote your question))... I'm a little unclear how this is structured... It looks like you have a ViewController that contains a view with a ProgressBar, and you are showing that (via modal segue, I guess?)... and while that view is being shown modally, your presenting controller is calling GetData.start() which is async retrieving new data? What happens if you simply show a ProgressBar as a subview, instead of as part of a modal presentation? – DonMag Mar 28 '17 at 12:20
  • Thanks for the help Don. Your view of my code structure is correct. I tried you suggestion and programmatically made the progress bar but sadly, the same result occurred. I have since managed to get the progress bar to appear at the start when it should but now it is not being updated, despite the updates being correctly reported via the print statements in the protocol method. I'll put the details in an edit section above. – Rossco Mar 28 '17 at 22:14
  • hmmm... I see `if percentComplete == 100 { }` ... UIProgressView is expecting a value between 0.0 and 1.0 (inclusive). Is it possible the issue has been that `percentComplete` is in `0 to 100`, instead of `0.0 to 1.0`? – DonMag Mar 29 '17 at 12:15
  • I was hoping that was going to be it but alas, no behavioural change after I changed the progress limits to 0.0 and 1.0. I have, however, made some progress. See the second edit above. – Rossco Mar 29 '17 at 22:02
  • There's gotta be something obvious being overlooked... Is it possible your `GetData` process is *not* running on a background thread? That could explain it... – DonMag Mar 30 '17 at 11:55
  • At the point of the delegate method call in GetData, the debugger is showing the code running on thread 3. Within the actual protocol method, I have place the updating lines of code in dispatchQueue.main.async block and the debugger reflects this as the code in the block is shown to be running on thread 1. It does not update at the time of calling though. Right after all delegate calls have been made, there is a gap of a couple of seconds and then the progress bar shoots straight to 100%. – Rossco Mar 30 '17 at 12:10
  • Well, I think we've run through the usual suspects... I'd be happy to put another set of eyes on it if it's in a "shareable" state... – DonMag Mar 30 '17 at 12:14
  • Finally! Solved it! Thanks very much for your help Don - much appreciated! I wouldn't call the solution obvious (for me anyways) but see the last edit above for what corrected things. Thanks again! – Rossco Mar 30 '17 at 20:31
  • Yep... sounds like you were initiating a lock; then telling the UI to update (setting the progress bar) while you had the lock; then unlocking and immediately locking again, before the UI could do anything. Not sure what all you're doing, but it sounds like you should re-think the approach. – DonMag Mar 30 '17 at 20:48

1 Answers1

0

I was using semaphores to ensure that data from the API calls were processed in the correct order. The solution was to place the "semaphore.signal()" statement BEFORE the call to the delegate method. Originally, the delegate call lay between "let semaphore = DispatchSemaphore(value: 0)" and "semaphore.signal()". Somehow (I don't understand it yet), this was causing the UI updates to queue up and not execute until all of the API data had finished processing.

Rossco
  • 37
  • 5