SetActive() can only be called from the main thread

So basically UI elements need to be modified in Main thread, and I found this script and it will execute your function in Main thread, just put your function in a Coroutine and Enqueue it to the script(UnityMainThreadDispatcher). (You need an object in the scene and add the MainThreadDispathcer script to it)

Here's how my Function looked:

public void SignInWithEmail()
{
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
     DatabaseReference.GetValueAsync().ContinueWith(task => {
         //Here's the fix
         UnityMainThreadDispatcher.Instance().Enqueue(ShowUserPanel());
    }
  }
}


public IEnumerator ShowUserPanel()
{
    uiController.userPanel.panel.SetActive(true);
    uiController.authPanel.SetActive(false);
    yield return null;
}

This is the script to run it in Main Thead

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;


public class UnityMainThreadDispatcher : MonoBehaviour {

private static readonly Queue<Action> _executionQueue = new Queue<Action>();


/// <summary>
/// Locks the queue and adds the IEnumerator to the queue
/// </summary>
/// <param name="action">IEnumerator function that will be executed from the main thread.</param>
public void Enqueue(IEnumerator action) {
    lock (_executionQueue) {
        _executionQueue.Enqueue (() => {
            StartCoroutine (action);
        });
    }
}

/// <summary>
/// Locks the queue and adds the Action to the queue
/// </summary>
/// <param name="action">function that will be executed from the main thread.</param>
public void Enqueue(Action action)
{
    Enqueue(ActionWrapper(action));
}
IEnumerator ActionWrapper(Action a)
{
    a();
    yield return null;
}


private static UnityMainThreadDispatcher _instance = null;

public static bool Exists() {
    return _instance != null;
}

public static UnityMainThreadDispatcher Instance() {
    if (!Exists ()) {
        throw new Exception ("UnityMainThreadDispatcher could not find the UnityMainThreadDispatcher object. Please ensure you have added the MainThreadExecutor Prefab to your scene.");
    }
    return _instance;
}

void Awake() {
    if (_instance == null) {
        _instance = this;
        DontDestroyOnLoad(this.gameObject);
    }
}

public void Update() {
    lock(_executionQueue) {
        while (_executionQueue.Count > 0) {
            _executionQueue.Dequeue().Invoke();
        }
    }
}

void OnDestroy() {
        _instance = null;
}

}

Note that Firebase has now a nice ContinueWithOnMainThread extension method that solves this problem more elegantly than the other suggested answers:

using Firebase.Extensions;
public void SignInWithEmail() {
    // This code runs on the caller's thread.
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {
      // This code runs on an arbitrary thread.
      DatabaseReference.GetValueAsync().ContinueWithOnMainThread(task => {
        // This code runs on the Main thread. No problem.
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    }
  }
}```

So my answer is very similar to the accepted answer from Milod's, but a little different, as it took me a while to wrap my head around his, even though his still works.

  1. The Issue: Normally, all your code runs on a single thread in Unity, since Unity is single-threaded, however when working with APIs like Firebase, which require callbacks, the callback functions will be handled by a new thread. This can lead to race-conditions, especially on a single-threaded engine like Unity.

  2. The solution (from Unity): Starting from Unity 2017.X, unity now requires changes to UI components to be run on the Main thread (i.e. the first thread that was started with Unity).

  3. What is impacted ?: Mainly calls that modify the UI like...

    gameObject.SetActive(true);  // (or false)
    textObject.Text = "some string" // (from UnityEngine.UI)
    
  4. How this relates to your code:

public void SignInWithEmail() {
    // auth.SignInWithEmailAndPasswordAsyn() is run on the local thread, 
    // ...so no issues here
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {

     // .ContinueWith() is an asynchronous call 
     // ...to the lambda function defined within the  task=> { }
     // and most importantly, it will be run on a different thread, hence the issue
      DatabaseReference.GetValueAsync().ContinueWith(task => {

        //HERE IS THE PROBLEM 
        userPanel.SetActive(true);
        authPanel.SetActive(false);
    }
  }
}

  1. Suggested Solution: For those calls which require callback functions, like...
DatabaseReference.GetValueAsync()

...you can...

  • send them to a function which is set up to run on that initial thread.
  • ...and which uses a queue to ensure that they will be run in the order that they were added.
  • ...and using the singleton pattern, in the way advised by the Unity team.

Actual solution

  1. Place the code below into your scene on a gameObject that will always be enabled, so that you have a worker that...
    • always runs on the local thread
    • can be sent those callback functions to be run on the local thread.
using System;
using System.Collections.Generic;
using UnityEngine;

internal class UnityMainThread : MonoBehaviour
{
    internal static UnityMainThread wkr;
    Queue<Action> jobs = new Queue<Action>();

    void Awake() {
        wkr = this;
    }

    void Update() {
        while (jobs.Count > 0) 
            jobs.Dequeue().Invoke();
    }

    internal void AddJob(Action newJob) {
        jobs.Enqueue(newJob);
    }
}

  1. Now from your code, you can simply call...

     UnityMainThread.wkr.AddJob();
    

...so that your code remains easy to read (and manage), as shown below...

public void SignInWithEmail() {
    auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(task => {

      DatabaseReference.GetValueAsync().ContinueWith(task => {
        UnityMainThread.wkr.AddJob(() => {
            // Will run on main thread, hence issue is solved
            userPanel.SetActive(true);
            authPanel.SetActive(false);            
        })

    }
  }
}