Trabajar con el subproceso de interfaz de usuario en Xamarin.iOS

Las interfaces de usuario de la aplicación siempre son procesos únicos, incluso en dispositivos de varios subprocesos: solo hay una representación de la pantalla y los cambios en lo que se muestra deben coordinarse a través de un único "punto de acceso". Esto impide que varios subprocesos intenten actualizar el mismo píxel al mismo tiempo (por ejemplo).

El código solo debe realizar cambios en los controles de la interfaz de usuario del subproceso (o interfaz de usuario) principal. Es posible que las actualizaciones de la interfaz de usuario que se produzcan en un subproceso diferente (como una devolución de llamada o un subproceso en segundo plano) no se representen en la pantalla o puedan incluso provocar un bloqueo.

Ejecución de subprocesos de la interfaz de usuario

Al crear controles en una vista o controlar un evento iniciado por el usuario, como una entrada táctil, el código ya se está ejecutando en el contexto del subproceso de la interfaz de usuario.

Si el código se ejecuta en un subproceso en segundo plano, en una tarea o en una devolución de llamada, es probable que NO se ejecute en el subproceso principal de la interfaz de usuario. En este caso, debe encapsular el código en una llamada a InvokeOnMainThread o BeginInvokeOnMainThread de la siguiente manera:

InvokeOnMainThread ( () => {
    // manipulate UI controls
});

El método InvokeOnMainThread se define en NSObject para que se pueda llamar desde dentro de los métodos definidos en cualquier objeto UIKit (como una vista o un controlador de vista).

Al depurar las aplicaciones de Xamarin.iOS se producirá un error si el código intenta acceder a un control de interfaz de usuario desde el subproceso incorrecto. Esto le ayuda a rastrear y corregir estos problemas con el método InvokeOnMainThread. Esto solo se produce durante la depuración y no produce errores en las compilaciones de versión. El mensaje de error aparecerá de la siguiente manera:

UI Thread Execution

Ejemplo de subproceso en segundo plano

Este es un ejemplo que intenta acceder a un control de interfaz de usuario (una UILabel) desde un subproceso en segundo plano mediante un subproceso simple:

new System.Threading.Thread(new System.Threading.ThreadStart(() => {
    label1.Text = "updated in thread"; // should NOT reference UILabel on background thread!
})).Start();

Ese código iniciará la UIKitThreadAccessException durante la depuración. Para corregir el problema (y asegurarse de que solo se tiene acceso al control de interfaz de usuario desde el subproceso principal de la interfaz de usuario), encapsule cualquier código que haga referencia a los controles de interfaz de usuario dentro de una expresión InvokeOnMainThread como esta:

new System.Threading.Thread(new System.Threading.ThreadStart(() => {
    InvokeOnMainThread (() => {
        label1.Text = "updated in thread"; // this works!
    });
})).Start();

No es necesario usar esto durante el resto de los ejemplos de este documento, pero es un concepto importante a recordar cuándo la aplicación realice solicitudes de red, use el centro de notificaciones u otros métodos que requieran un controlador de finalización que se ejecute en otro subproceso.

Ejemplo de Async/Await

Cuando se usan las palabras clave async/await de C# 5 no se requiere InvokeOnMainThread porque cuando una tarea esperada completa el método, continúa en el subproceso que realiza la llamada.

Este código de ejemplo (que espera en una llamada al método Delay, exclusivamente con fines de demostración) muestra un método asincrónico al que se llama en el subproceso de la interfaz de usuario (es un controlador TouchUpInside). Dado que se llama al método contenedor en el subproceso de la interfaz de usuario, las operaciones de interfaz de usuario, como establecer el texto en UILabel o mostrar un elemento UIAlertView, se pueden llamar de forma segura después de que las operaciones asincrónicas se hayan completado en los subprocesos en segundo plano.

async partial void button2_TouchUpInside (UIButton sender)
{
    textfield1.ResignFirstResponder ();
    textfield2.ResignFirstResponder ();
    textview1.ResignFirstResponder ();
    label1.Text = "async method started";
    await Task.Delay(1000); // example purpose only
    label1.Text = "1 second passed";
    await Task.Delay(2000);
    label1.Text = "2 more seconds passed";
    await Task.Delay(1000);
    new UIAlertView("Async method complete", "This method", 
               null, "Cancel", null)
        .Show();
    label1.Text = "async method completed";
}

Si se llama a un método asincrónico desde un subproceso en segundo plano (no desde el subproceso principal de la interfaz de usuario), InvokeOnMainThread seguirá siendo necesario.