Widgets de IOS 14: características y limitaciones





Este año, hay varias oportunidades interesantes para que los desarrolladores de iOS agoten la batería del iPhone para mejorar la experiencia del usuario, una de las cuales son los nuevos widgets. Mientras todos esperamos la versión de lanzamiento del sistema operativo, me gustaría compartir mi experiencia al escribir un widget para la aplicación "Wallet" y decirles qué oportunidades y limitaciones encontró nuestro equipo en las versiones beta de Xcode.



Comencemos con la definición: los widgets son vistas que muestran información relevante sin iniciar la aplicación móvil principal y siempre están al alcance de la mano del usuario. La capacidad de usarlos ya existe en iOS ( Today Extension ), comenzando con iOS 8, pero mi experiencia puramente personal de usarlos es bastante triste: aunque se les asigna un escritorio especial con widgets, todavía rara vez llego allí, el hábito no se ha desarrollado.



Como resultado, en iOS 14 vemos un resurgimiento de widgets, más integrados en el ecosistema y más fáciles de usar (en teoría).







Trabajar con tarjetas de fidelización es una de las principales funciones de nuestra aplicación Wallet. De vez en cuando en las reseñas de la App Store hay sugerencias de los usuarios sobre la posibilidad de agregar un widget a Today. Los usuarios, estando en la caja, quisieran mostrar la tarjeta lo antes posible, obtener un descuento y huir de su negocio, porque el retraso en cualquier segmento de tiempo provoca esas miradas de reproche en la cola. En nuestro caso, el widget puede guardar varias acciones del usuario para abrir una tarjeta, lo que agiliza el pago de los productos en la caja. Las tiendas también se lo agradecerán: menos colas en la caja.



Este año, Apple lanzó inesperadamente un lanzamiento de iOS casi inmediatamente después de la presentación, dejando a los desarrolladores un día para finalizar sus aplicaciones en Xcode GM, pero estábamos listos para el lanzamiento, ya que nuestro equipo de iOS comenzó a hacer su propia versión del widget en versiones beta de Xcode. ... El widget se está revisando actualmente en la App Store. Según las estadísticas , la actualización de los dispositivos al nuevo iOS es bastante rápida ; Lo más probable es que los usuarios vayan a comprobar qué aplicaciones ya tienen widgets, encontrarán los nuestros y se alegrarán.



En el futuro, nos gustaría agregar información aún más relevante, por ejemplo, saldo, código de barras, últimos mensajes no leídos de socios y notificaciones (por ejemplo, que los usuarios deben tomar una acción: confirmar o activar la tarjeta). Por el momento, el resultado se ve así:







Agregar un widget a un proyecto



Al igual que otras características adicionales similares, el widget se agrega como una extensión del proyecto principal. Una vez agregado, Xcode ha generado amablemente el código para el widget y otras clases principales. Aquí es donde nos esperaba la primera característica interesante: para nuestro proyecto, este código no se compiló, ya que en uno de los archivos se insertó automáticamente un prefijo en los nombres de las clases (sí, esos mismos prefijos de Obj-C), pero no en los archivos generados. Como dice el refrán, no son los dioses los que queman las ollas, al parecer, los diferentes equipos dentro de Apple no se pusieron de acuerdo entre ellos. Esperemos que lo arreglen para la versión de lanzamiento. Para personalizar el prefijo de su proyecto, en el Inspector de archivos del objetivo principal de la aplicación, complete el campo Prefijo de clase .



Para aquellos que han seguido las novedades de la WWDC, no es ningún secreto que la implementación de widgets solo es posible usando SwiftUI. Un punto interesante es que, de esta manera, Apple está forzando una actualización de sus tecnologías: incluso si la aplicación principal está escrita con UIKit, entonces, por favor, solo SwiftUI. Por otro lado, esta es una buena oportunidad para probar un nuevo marco para escribir una función, en este caso encaja cómodamente en el proceso: sin cambios de estado, sin navegación, solo necesita declarar una IU estática. Es decir, junto con el nuevo marco, también han aparecido nuevas restricciones, porque los viejos widgets en Today pueden contener más lógica y animación.



Una de las principales innovaciones en SwiftUI es la capacidad de obtener una vista previa sin iniciarlo en un simulador o dispositivo ( vista previa ). Algo genial, pero, desafortunadamente, en proyectos grandes (en el nuestro, ~ 400K líneas de código) funciona extremadamente lento incluso en las mejores MacBooks, es más rápido de ejecutar en un dispositivo. Una alternativa a esto es tener un proyecto vacío o un patio de juegos a mano para la creación rápida de prototipos.



La depuración también está disponible con un esquema de Xcode dedicado. En el simulador, la depuración es inestable incluso en la versión Xcode 12 beta 6, por lo que es mejor donar uno de los dispositivos de prueba, actualizar a iOS 14 y probarlo. Esté preparado para que esta parte no funcione como se esperaba en las versiones de lanzamiento.



Interfaz



El usuario puede elegir entre diferentes tipos ( WidgetFamily ) de widgets de tres tamaños: pequeño, mediano y grande .







Para registrarse, debe especificar explícitamente el soporte:

struct CardListWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: “CardListWidgetKind”,
                            intent: DynamicMultiSelectionIntent.self,
                            provider: CardListProvider()) { entry in
            CardListEntryView(entry: entry)
        }
        .configurationDisplayName(" ")
        .description(",     ")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}


Mi equipo y yo decidimos quedarnos con lo pequeño y lo mediano: mostrar una tarjeta favorita para un widget pequeño o 4 para un widget mediano.



El widget se agrega al escritorio desde el centro de control, donde el usuario elige el tipo que necesita:







Personalice el color del botón "Agregar widget" usando Assets.xcassets -> AccentColor , el nombre del widget con una descripción también (código de ejemplo arriba).



Si se encuentra con la limitación en la cantidad de vistas admitidas, puede expandirla usando el WidgetBundle :



@main
struct WalletBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        CardListWidget()
        MySecondWidget()
    }
}


Dado que el widget muestra una instantánea de algún estado, la única posibilidad de interacción del usuario es cambiar a la aplicación principal haciendo clic en algún elemento o en todo el widget. Sin animación, navegación ni transiciones a otras vistas . Pero es posible colocar un enlace profundo en la aplicación principal. En este caso, para un widget pequeño , la zona de clic es el área completa, y en este caso usamos el método widgetURL (_ :) . Por medio y grande, vista clics están disponibles , y el Enlace estructura de SwiftUI nos van a ayudar con esto .



Link(destination: card.url) {
  CardView(card: card)
}


La vista final del widget de dos tamaños resultó de la siguiente manera:







Al diseñar la interfaz del widget, las siguientes reglas y requisitos pueden ayudar (de acuerdo con las pautas de Apple):

  1. Enfoque el widget en una idea y problema, no intente repetir toda la funcionalidad de la aplicación.
  2. Muestra más información según el tamaño, en lugar de simplemente escalar el contenido.
  3. Muestra información dinámica que puede cambiar a lo largo del día. Los extremos en forma de información completamente estática e información que cambia cada minuto no son bienvenidos.
  4. El widget debe proporcionar información relevante a los usuarios y no ser otra forma de abrir la aplicación.


La apariencia se ha personalizado. El siguiente paso es elegir qué tarjetas mostrar al usuario y cómo. Claramente puede haber más de cuatro cartas. Consideremos varias opciones:

  1. Permitir que el usuario elija tarjetas. ¡Quién, si no él, sabe qué cartas son más importantes!
  2. Muestra los últimos mapas usados.
  3. Haz un algoritmo más inteligente, enfocándote, por ejemplo, en la hora y el día de la semana y las estadísticas (si un usuario va a una frutería cercana a la casa entre semana y va a un hipermercado los fines de semana, entonces puedes ayudar al usuario en ese momento y mostrar la tarjeta deseada)


Como parte del prototipo, nos decidimos por la primera opción para, al mismo tiempo, probar la capacidad de ajustar los parámetros directamente en el widget. No es necesario hacer una pantalla especial dentro de la aplicación. ¿Es cierto que los usuarios, como dicen, tienen la experiencia suficiente para encontrar estos ajustes?



Configuración de widget personalizada



La configuración se genera usando intenciones (hola desarrolladores de Android): al crear un nuevo widget, el archivo de intenciones se agrega automáticamente al proyecto. El generador de código preparará una clase heredada de INIntent , que es parte del marco SiriKit . Los parámetros de la intención contienen la opción mágica "La intención es elegible para widgets" . Hay varios tipos de parámetros disponibles, puede personalizar sus subtipos. Dado que los datos en nuestro caso son una lista dinámica, también configuramos el elemento "Las opciones se proporcionan dinámicamente" .



Para diferentes tipos de widgets, establezca el número máximo de elementos en la lista: para 1 pequeño, para mediano 4.

El widget utiliza este tipo de intención como fuente de datos.







A continuación, la clase de intención configurada debe colocarse en la configuración de IntentConfiguration .

struct CardListWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: WidgetConstants.widgetKind,
                            intent: DynamicMultiSelectionIntent.self,
                            provider: CardListProvider()) { entry in
            CardListEntryView(entry: entry)
        }
        .configurationDisplayName(" ")
        .description(",     .")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}


Si no se requiere la configuración del usuario, existe una alternativa en forma de la clase StaticConfiguration, que funciona sin especificar una intención.



El título y la descripción se pueden editar en la pantalla de configuración.

El nombre del widget debe caber en una línea; de lo contrario, se cortará. Al mismo tiempo, la longitud permitida para la pantalla de adición y la configuración del widget son diferentes.

Ejemplos de la longitud máxima del nombre para algunos dispositivos:



iPhone 11 Pro Max
28  
21   

iPhone 11 Pro
25  
19   

iPhone SE
24  
19   


La descripción es de varias líneas. En el caso de texto muy largo en la configuración, el contenido se puede desplazar. Pero en la pantalla de agregar, primero se comprime la vista previa del widget y luego sucede algo terrible con el diseño.







También puede cambiar el color de fondo y los valores de los parámetros WidgetBackground y AccentColor; de forma predeterminada, ya están en Activos . Si es necesario, se pueden renombrar en la configuración del widget en Configuración de compilación en el Compilador del catálogo de activos - Grupo Opciones en los campos Nombre del color de fondo del widget y Nombre del color de acento global , respectivamente.







Algunos parámetros se pueden ocultar (o mostrar) según el valor seleccionado en otro parámetro a través de la configuración de Relación .

Cabe señalar que la interfaz de usuario para editar un parámetro depende de su tipo. Por ejemplo, si especificamos Boolean , entonces veremos UISwitch , y si Integer , entonces ya tenemos una opción de dos opciones: entrada vía UITextfield o cambio paso a paso vía UIStepper .







Interacción con la aplicación principal.



El paquete se ha configurado, queda por determinar de dónde la intención tomará los datos reales. El puente con la aplicación principal en este caso es un archivo en el grupo general ( Grupos de aplicaciones ). La aplicación principal escribe, el widget lee.

El siguiente método se utiliza para obtener la URL del grupo general:

FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: “group.ru.yourcompany.yourawesomeapp”)


Guardamos todos los candidatos, ya que serán utilizados por el usuario en la configuración como un diccionario para la selección.

A continuación, el sistema operativo debe averiguar que los datos han sido actualizados, para esto llamamos:

WidgetCenter.shared.reloadAllTimelines()
//  WidgetCenter.shared.reloadTimelines(ofKind: "kind")


Dado que la llamada al método recargará el contenido del widget y toda la línea de tiempo, utilícelo cuando los datos se hayan actualizado para no sobrecargar el sistema.



Actualizar datos



Para cuidar la batería de un dispositivo de usuario, Apple ha ideado un mecanismo para actualizar los datos en un widget usando una línea de tiempo , un mecanismo para generar instantáneas . El desarrollador no actualiza ni administra directamente la vista , sino que proporciona un programa, guiado por el cual, el sistema operativo cortará instantáneas en segundo plano.

La actualización tiene lugar en los siguientes eventos:

  1. Llamar al WidgetCenter.shared.reloadAllTimelines () utilizado anteriormente
  2. Cuando un usuario agrega un widget al escritorio
  3. Al editar la configuración.


Además, el desarrollador tiene tres tipos de políticas para actualizar las líneas de tiempo (TimelineReloadPolicy):

atEnd : actualizar después de mostrar la última instantánea

nunca : actualizar solo en caso de una llamada forzada

después de (_ :) : actualizar después de un cierto período de tiempo.



En nuestro caso, basta con pedirle al sistema que tome una instantánea hasta que se actualicen los datos de la tarjeta en la aplicación principal:



struct CardListProvider: IntentTimelineProvider {
    public typealias Intent = DynamicMultiSelectionIntent
    public typealias Entry = CardListEntry

    public func placeholder(in context: Context) -> Self.Entry {
        return CardListEntry(date: Date(), cards: testData)
    }

    public func getSnapshot(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Self.Entry) -> Void) {
        let entry = CardListEntry(date: Date(), cards: testData)
        completion(entry)
    }

    public func getTimeline(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) {
        let cards: [WidgetCard]? = configuration.cards?.compactMap { card in
            let id = card.identifier
            let storedCards = SharedStorage.widgetRepository.restore()
            return storedCards.first(where: { widgetCard in widgetCard.id == id })
        }

        let entry = CardListEntry(date: Date(), cards: cards ?? [])
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

struct CardListEntry: TimelineEntry {
    public let date: Date
    public let cards: [WidgetCard]
}


Una opción más flexible sería útil si se utiliza un algoritmo automático para seleccionar tarjetas según el día de la semana y la hora.



Por separado, vale la pena señalar la visualización de un widget si está en una pila de widgets ( Smart Stack ). En este caso, podemos usar dos opciones para administrar las prioridades: Siri Suggestions o estableciendo el valor de relevancia de un TimelineEntry con el tipo TimelineEntryRelevance . TimelineEntryRelevance contiene dos parámetros:

puntuación : la prioridad de la instantánea actual en relación con otras instantáneas;

duración es el tiempo hasta que el widget sigue siendo relevante y el sistema puede colocarlo en la primera posición de la pila.



Ambos métodos, así como las opciones de configuración del widget, se discutieron en detalle en la sesión de la WWDC .



También debe hablar sobre cómo mantener actualizada la visualización de la fecha y la hora. Dado que no podemos actualizar regularmente el contenido del widget, se agregaron varios estilos para el componente Texto. Cuando se usa un estilo, el sistema actualiza automáticamente el contenido del componente mientras el widget está en la pantalla. Quizás en el futuro el mismo enfoque se extenderá a otros componentes de SwiftUI.



El texto admite los siguientes estilos:

relativo- la diferencia horaria entre la fecha actual y la especificada. Vale la pena señalar aquí: si la fecha se especifica en el futuro, entonces comienza la cuenta regresiva, y luego se muestra la fecha desde el momento en que llega a cero. El mismo comportamiento será para los siguientes dos estilos;

desplazamiento : similar al anterior, pero hay una indicación en forma de prefijo con ±;

temporizador - analógico de un temporizador;

fecha - visualización de la fecha ;

tiempo - visualización del tiempo .



Además, es posible mostrar el intervalo de tiempo entre fechas simplemente especificando el intervalo.



let components = DateComponents(minute: 10, second: 0)
 let futureDate = Calendar.current.date(byAdding: components, to: Date())!
 VStack {
   Text(futureDate, style: .relative)
      .multilineTextAlignment(.center)
   Text(futureDate, style: .offset)
      .multilineTextAlignment(.center)
   Text(futureDate, style: .timer)
      .multilineTextAlignment(.center)
   Text(Date(), style: .date) 
      .multilineTextAlignment(.center)
   Text(Date(), style: .time)
      .multilineTextAlignment(.center)
   Text(Date() ... futureDate)
      .multilineTextAlignment(.center)
}






Vista previa del widget



Cuando se muestra por primera vez, el widget se abrirá en modo de vista previa, para esto necesitamos devolver TimeLineEntry en el marcador de posición (en el método :). En nuestro caso, se ve así:

func placeholder(in context: Context) -> Self.Entry {
        return CardListEntry(date: Date(), cards: testData)
 }


Después de eso, el modificador redactado (motivo :) con el parámetro de marcador de posición se aplica a la vista . En este caso, los elementos del widget se muestran borrosos.







Podemos eliminar este efecto de algunos elementos usando el modificador unredacted () .

La documentación también dice que la llamada al método de marcador de posición (en :) es sincrónica y el resultado debería regresar lo más rápido posible, a diferencia de getSnapshot (in: completamiento :) y getTimeline (in: completado :)



Elementos de redondeo



En las pautas, se recomienda hacer coincidir el redondeo de elementos con el redondeo del widget; para ello , se agregó la estructura ContainerRelativeShape en iOS 14 , que permite aplicar la forma de un contenedor a una vista.



.clipShape(ContainerRelativeShape()) 


Soporte de Objective-C



Si necesita agregar código Objective-C al widget (por ejemplo, hemos escrito la generación de imágenes de códigos de barras en él), todo sucede de la manera estándar agregando el encabezado puente de Objective-C. El único problema con el que nos encontramos fue que durante la compilación, Xcode dejó de ver los archivos de intención generados automáticamente, por lo que también los agregamos al encabezado de puente :



#import "DynamicCardSelectionIntent.h"
#import "CardSelectionIntent.h"
#import "DynamicMultiSelectionIntent.h"


Tamaño de la aplicación



Las pruebas se realizaron en Xcode 12 beta 6

Sin widget: 61.6 MB

Con un widget: 62.2 MB



Resumiré los puntos principales que se discutieron en el artículo:

  1. Los widgets son una excelente manera de familiarizarse con SwiftUI en la práctica. Agréguelos a su proyecto incluso si la versión mínima admitida es inferior a iOS 14.
  2. WidgetBundle se usa para aumentar la cantidad de widgets disponibles, aquí hay un gran ejemplo de cuántos widgets diferentes tiene ApolloReddit.
  3. IntentConfiguration o StaticConfiguration ayudarán a agregar configuraciones personalizadas en el widget si no se necesitan configuraciones personalizadas.
  4. Una carpeta compartida en el sistema de archivos en los grupos de aplicaciones compartidos lo ayudará a sincronizar los datos con la aplicación principal.
  5. El desarrollador puede elegir entre varias políticas para actualizar la línea de tiempo (al final, nunca, después de (_ :)).


En esto, el camino espinoso de desarrollar un widget en versiones beta de Xcode puede considerarse completo, solo queda un paso simple: revisar una revisión en la App Store.



PD ¡La versión con el widget ha pasado la moderación y ahora está disponible para descargar en la App Store!



Gracias por leer hasta el final, estaré encantado de recibir sugerencias y comentarios. Realice una breve encuesta para ver qué tan populares son los widgets entre usuarios y desarrolladores.



All Articles