Problem with UIActivityIndicator in Navigation Bar

I have an UIActivityIndicator (_activityIndicator) as the right bar button item in the navigation bar. When my UITableViewController appears (in ViewWillAppear) I do:

_activityIndicator.StartAnimating ();

Thereafter (in ViewDidAppear) I load the tableview with data from a server. This works fine, i.e. _activityIndicator spins.

I also have refresh button as the left bar button item in the navigation bar. When the user clicks this button, the tableview is refreshed, again with data from a server. The problem here is that I can't get the _activityIndicator to spin again. The following code (handler for the refresh button click) does not work, i.e. the _activityIndicator does not appear (let alone spin):

private void ReloadStuff(object sender, EventArgs args) {
        _activityIndicator.StartAnimating();    
        _loader.LoadStuff ();
        UpdateTableView (_loader.Stuff);
        _activityIndicator.StopAnimating();
}

If I move the statement _activityIndicator.StartAnimating(); after UpdateTableView (_loader.Stuff); then I see it spinning.

Hence, it seems that problem is that the navigation bar is not redrawn until after the table view has been redrawn.

Any suggestions on how this can be fixed?

Posts

  • rschmidtrschmidt USMember ✭✭

    What happens if you comment out this line

    _activityIndicator.StopAnimating();

    You're telling the activity indicator to start animating and then to stop animating, without returning to the run loop in between, so it will never start animating on the screen. Also, your LoadStuff() method appears to be synchronous, so your code is going to lock up the GUI while it runs.

    Try something more like this:

    private void ReloadStuff(object sender, EventArgs args)
    {
            _activityIndicator.StartAnimating();
            _loader.StuffLoaded += this.OnLoadStuff;
            _loader.LoadStuff ();
    }
    
    private void OnLoadStuff(object sender, EventArgs e)
    {
            UpdateTableView (_loader.Stuff);
            _activityIndicator.StopAnimating();
    }
    
  • adamkempadamkemp USInsider, Developer Group Leader mod

    Randall is correct. You can also do it using async/await like this:

    private void ReloadStuff()
    {
        _activityIndicator.StartAnimation();
        await Task.Yield();
        _loader.LoadStuff ();
        UpdateTableView (_loader.Stuff);
        _activityIndicator.StopAnimating();
    }
    

    That still does the reload in the UI thread, which isn't ideal, but if you make it fully async then you probably need to do some other work (like disabling UI elements during the reload to avoid getting into bad states). The above approach should fix your animation issue even though your reload function is still running in the UI thread.

  • HrafnLoftssonHrafnLoftsson ISMember ✭✭

    @rschmidt‌: If a comment out this line
    _activityIndicator.StopAnimating();
    the indicator starts spinning after the table has been loaded.

    Can you please explain why your proposed solution should work? I mean what is the real difference between your proposal and the original code? If I understand it correctly, StuffLoaded is an EventHandler which gets fired after at the end of _loader.LoadStuff().

    I tried this, but the indicator does not spin.

  • HrafnLoftssonHrafnLoftsson ISMember ✭✭

    @adamkemp‌ : Your proposal works, but only when I click the refresh button the first time! When I refresh the second time the indicator does not appear. Any idea what can explain this?

  • adamkempadamkemp USInsider, Developer Group Leader mod

    No, that doesn't make sense. I think I'm probably missing some context that could explain it.

  • rschmidtrschmidt USMember ✭✭

    @HrafnLoftsson‌

    For it to work, your LoadStuff method would need to be asynchronous.

    StartAnimating() does not start the animation right then and there. It schedules it to happen after your code finishes executing and returns to the run loop. So even though you call StartAnimating() at the beginning of your ReloadStuff event handler, the animation doesn't start on screen until after all the code in that event handler finishes executing and returns to the run loop. In other words, the animation will not start until after the last line of the event handler, _activityIndicator.StopAnimating(); runs!

    Clearly that is a problem. Since you call StopAnimating() before your code finishes executing, that effectively cancels out the call to StartAnimating(). (I'm not sure if it truly cancels the animation, or if the animation does start but then immediately stops).

    The trick then is to call StartAnimating(), then return to the run loop quickly, either before your LoadStuff() method gets called (like in Adam's solution) or while it's executing in a background thread (like in mine).

  • HrafnLoftssonHrafnLoftsson ISMember ✭✭

    @rschmidt‌

    I have changed the LoadStuff to an asynchronous method:

    private async void ReloadStuff() {
                _activityIndicator.StartAnimating();
                await _loader.LoadStuff ();
                UpdateTableView (_loader.Stuff);
                _activityIndicator.StopAnimating();
            }
    

    When the refresh button is clicked the ReloadStuff() method is called.
    If I understand this correctly, once await _loader.LoadStuff() is executed control is returned to the calling method (the button click), i.e. to the run loop. Once LoadStuff() finishes the remainder of ReloadStuff() is executed. After this change LoadStuff() and UpdateTableView() still work, but the _activityIndicator does not spin.

    If I comment out _activityIndicator.StopAnimating(); I can see it spinning, only after the table has been loaded!

    Any further suggestions to try?

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Show us how you made LoadStuff asynchronous.

  • HrafnLoftssonHrafnLoftsson ISMember ✭✭

    The matter is solved!

    The LoadStuff() method was defined like this:

    public async Task LoadStuff ()
    {
    
        // some code
        ...
        await GetData ();
    }
    

    The problem was that the GetData() method was not asynchronous (despite using async in the function header) because it lacked an await operator.

    Wrapping the call to GetData into Task.Run() solved the problem:

    public async Task LoadStuff ()
    {
    
        // some code
        ...
        await Task.Run( () => { GetData (); });
    }
    

    Thanks for the help, @rschmidt‌ and @adamkemp‌ !

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Ah, I figured it would be something like that. A lot of people get confused by that. async does not make your function actually run in the background. It just allows it to use await. I'm glad you got it working.

  • KMullinsKMullins USMember, Xamarin Team Xamurai

    For anybody that happens to get stuck using async...await, we have a nice document on the topic that should help out, Async Support Overview.

    Thanks,

    Kevin

Sign In or Register to comment.