
Este ano, existem várias oportunidades interessantes para os desenvolvedores de iOS
Vamos começar com a definição - widgets são visualizações que mostram informações relevantes sem iniciar o aplicativo móvel principal e estão sempre ao alcance do usuário. A capacidade de usá-los já existe no iOS ( extensão Hoje ), começando com o iOS 8, mas minha experiência puramente pessoal de usá-los é bastante triste - embora um desktop especial com widgets seja alocado para eles, eu ainda raramente chego lá, o hábito não se desenvolveu.
Como resultado, no iOS 14, vemos um ressurgimento de widgets, mais integrados ao ecossistema e mais amigáveis (em teoria).

Trabalhar com cartões de fidelidade é uma das principais funções de nosso aplicativo Wallet. De vez em quando, sugestões de usuários sobre a possibilidade de adicionar um widget ao Hoje aparecem em resenhas na App Store. Os usuários, estando no caixa, gostariam de mostrar o cartão o quanto antes, conseguir um desconto e fugir com seus negócios, pois o atraso de qualquer fração de tempo causa aqueles olhares muito reprovadores na fila. No nosso caso, o widget pode salvar diversas ações do usuário ao abrir um cartão, tornando assim o pagamento da mercadoria no caixa. As lojas também ficarão gratas - menos filas no caixa.
Este ano, a Apple lançou inesperadamente uma versão iOS quase imediatamente após a apresentação, deixando aos desenvolvedores um dia para finalizar seus aplicativos no Xcode GM, mas estávamos prontos para o lançamento, já que nossa equipe iOS começou a fazer sua própria versão do widget em versões beta do Xcode ... O widget está atualmente sendo revisado na App Store. De acordo com as estatísticas , atualizar dispositivos para o novo iOS é muito rápido ; provavelmente, os usuários irão verificar quais aplicativos já possuem widgets, encontrar o nosso e ficar feliz.
No futuro, gostaríamos de adicionar informações ainda mais relevantes - por exemplo, saldo, código de barras, últimas mensagens não lidas de parceiros e notificações (por exemplo, que os usuários precisam realizar uma ação - para confirmar ou ativar o cartão). No momento, o resultado é o seguinte:

Adicionar um widget a um projeto
Como outros recursos adicionais semelhantes, o widget é adicionado como uma extensão do projeto principal. Uma vez adicionado, o Xcode gentilmente gerou o código para o widget e outras classes principais. É aqui que nos aguardava a primeira funcionalidade interessante - para o nosso projeto, este código não foi compilado, pois em um dos arquivos um prefixo foi inserido automaticamente nos nomes das classes (sim, esses mesmos prefixos Obj-C!), Mas não nos arquivos gerados. Como diz o ditado, não são os deuses que queimam as panelas, aparentemente, as diferentes equipes dentro da Apple não concordavam entre si. Esperamos que eles consertem para a versão de lançamento. Para personalizar o prefixo do seu projeto, no Inspetor de Arquivos do destino principal do aplicativo, preencha o campo Prefixo da Classe .
Para quem já acompanhou as novidades do WWDC, não é segredo que a implementação de widgets só é possível usando o SwiftUI. Um ponto interessante é que desta forma a Apple está forçando uma atualização de suas tecnologias: mesmo que o aplicativo principal seja escrito usando UIKit, então, se você quiser, apenas SwiftUI. Por outro lado, esta é uma boa oportunidade de experimentar um novo framework para escrever um recurso, neste caso ele se encaixa confortavelmente no processo - sem mudanças de estado, sem navegação, você só precisa declarar uma IU estática. Ou seja, junto com o novo framework, novas restrições surgiram, pois os widgets antigos do Today podem conter mais lógica e animação.
Uma das principais inovações no SwiftUI é a capacidade de visualizar sem iniciá-lo em um simulador ou dispositivo ( visualização ). Uma coisa legal, mas, infelizmente, em grandes projetos (nos nossos - ~ 400 mil linhas de código) ele funciona extremamente devagar, mesmo nos melhores MacBooks, é mais rápido para rodar em um dispositivo. Uma alternativa para isso é ter um projeto vazio ou playground disponível para prototipagem rápida.
A depuração também está disponível com um esquema Xcode dedicado. No simulador, a depuração é instável até mesmo até o Xcode 12 beta 6, então é melhor doar um dos dispositivos de teste, atualizar para o iOS 14 e testar nele. Esteja preparado para que esta parte não funcione conforme o esperado nas versões de lançamento.
Interface
O usuário pode escolher entre diferentes tipos ( WidgetFamily ) de widgets de três tamanhos - pequeno, médio, grande .

Para se registrar, você deve especificar explicitamente o compatível:
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])
}
}
Minha equipe e eu decidimos ficar com o pequeno e o médio - exibir um cartão favorito para um widget pequeno ou 4 para o médio.
Um widget é adicionado à área de trabalho a partir do centro de controle, onde o usuário escolhe o tipo de que precisa:

Personalize a cor do botão "Adicionar widget" usando Assets.xcassets -> AccentColor , o nome do widget com uma descrição também (código de exemplo acima).
Se você se deparar com a limitação do número de visualizações com suporte, pode expandi-la usando o WidgetBundle :
@main
struct WalletBundle: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
CardListWidget()
MySecondWidget()
}
}
Uma vez que o widget mostra um instantâneo de algum estado, a única possibilidade de interação do usuário é alternar para o aplicativo principal clicando em algum elemento ou em todo o widget. Sem animação, navegação ou transições para outras visualizações . Mas é possível inserir um link profundo no aplicativo principal. Nesse caso, para um widget pequeno , a zona de clique é toda a área e, neste caso, usamos o método widgetURL (_ :) . Para médio e grande, vista cliques estão disponíveis , eo link estrutura de SwiftUI vai nos ajudar com este .
Link(destination: card.url) {
CardView(card: card)
}
A visão final do widget de dois tamanhos resultou da seguinte maneira:

Ao projetar a interface do widget, as seguintes regras e requisitos podem ajudar (de acordo com as diretrizes da Apple):
- Foque o widget em uma ideia e problema, não tente repetir todas as funcionalidades do aplicativo.
- Exiba mais informações dependendo do tamanho, em vez de apenas dimensionar o conteúdo.
- Exibir informações dinâmicas que podem mudar ao longo do dia. Extremos na forma de informações completamente estáticas e informações que mudam a cada minuto não são bem-vindos.
- O widget deve fornecer informações relevantes aos usuários, não apenas outra maneira de abrir o aplicativo.
A aparência foi personalizada. A próxima etapa é escolher quais cartões mostrar ao usuário e como. Pode haver claramente mais de quatro cartas. Vamos considerar várias opções:
- Permitir que o usuário escolha os cartões. Quem, senão ele, sabe quais são as cartas mais importantes!
- Mostra os últimos mapas usados.
- Faça um algoritmo mais inteligente, focando, por exemplo, na hora e no dia da semana e nas estatísticas (se um usuário vai a uma frutaria perto de sua casa durante a semana à noite e vai a um hipermercado nos fins de semana, então você pode ajudar o usuário neste momento e mostrar o cartão desejado)
Como parte do protótipo, optamos pela primeira opção para, ao mesmo tempo, tentar a capacidade de configurar parâmetros diretamente no widget. Não há necessidade de fazer tela especial dentro do aplicativo. No entanto, os usuários, como dizem, são experientes o suficiente para encontrar essas configurações?
Configurações de widget personalizadas
As configurações são geradas usando intents (olá, desenvolvedores Android) - ao criar um novo widget, o arquivo de intent é adicionado automaticamente ao projeto. O gerador de código irá preparar uma classe herdada de INIntent , que faz parte do framework SiriKit . Os parâmetros de intent contêm a opção mágica "Intent é elegível para widgets" . Vários tipos de parâmetros estão disponíveis, você pode personalizar seus subtipos. Como os dados em nosso caso são uma lista dinâmica, também definimos o item "As opções são fornecidas dinamicamente" .
Para diferentes tipos de widget, defina o número máximo de itens na lista - para 1 pequeno, para médio 4.
Esse tipo de intenção é usado pelo widget como fonte de dados.

Em seguida, a classe de intent configurada deve ser colocada na configuração 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])
}
}
Se as configurações do usuário não forem necessárias, há uma alternativa na forma da classe StaticConfiguration, que funciona sem especificar uma intenção.
O título e a descrição são editáveis na tela de configurações.
O nome do widget deve caber em uma linha, caso contrário, será truncado. Nesse caso, o comprimento permitido para a tela de adição e as configurações do widget são diferentes.
Exemplos de comprimento máximo de nome para alguns dispositivos:
iPhone 11 Pro Max
28
21
iPhone 11 Pro
25
19
iPhone SE
24
19
A descrição é multilinha. No caso de texto muito longo nas configurações, o conteúdo pode ser rolado. Mas, na tela de adição, a visualização do widget é compactada primeiro e, em seguida, algo terrível acontece com o layout.

Você também pode alterar a cor de fundo e os valores dos WidgetBackground parâmetros e AccentColor - por padrão, eles já estão em Ativos . Se necessário, eles podem ser renomeados na configuração do widget em Build Settings no grupo Asset Catalog Compiler - Opções nos campos Widget Background Color Name e Global Accent Color Name , respectivamente.

Alguns parâmetros podem ser ocultados (ou mostrados) dependendo do valor selecionado em outro parâmetro por meio da configuração de Relacionamento .
Deve-se observar que a IU para editar um parâmetro depende de seu tipo. Por exemplo, se especificarmos Boolean , veremos UISwitch , e se Integer , já temos uma escolha de duas opções: entrada via UITextfield ou mudança passo a passo via UIStepper .

Interação com o aplicativo principal.
O pacote foi configurado, resta determinar de onde a própria intenção obterá os dados reais. A ponte com a aplicação principal, neste caso, é um arquivo do grupo geral ( Grupos de Aplicativos ). O aplicativo principal escreve, o widget lê.
O método a seguir é usado para obter o URL para o grupo geral:
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: “group.ru.yourcompany.yourawesomeapp”)
Salvamos todos os candidatos, pois eles serão usados pelo usuário nas configurações como um dicionário para seleção.
Em seguida, o sistema operacional deve descobrir que os dados foram atualizados, para isso chamamos:
WidgetCenter.shared.reloadAllTimelines()
// WidgetCenter.shared.reloadTimelines(ofKind: "kind")
Uma vez que a chamada do método recarregará o conteúdo do widget e toda a linha do tempo, use-o quando os dados tiverem sido realmente atualizados para não sobrecarregar o sistema.
Atualizando dados
Para cuidar da bateria de um dispositivo do usuário, a Apple planejou um mecanismo para atualizar os dados em um widget usando uma linha do tempo - um mecanismo para gerar instantâneos . O desenvolvedor não atualiza ou gerencia diretamente a visualização , mas em vez disso fornece uma programação, guiada pela qual o sistema operacional cortará os instantâneos em segundo plano.
A atualização ocorre nos seguintes eventos:
- Chamando o WidgetCenter.shared.reloadAllTimelines () usado anteriormente
- Quando um usuário adiciona um widget à área de trabalho
- Ao editar as configurações.
Além disso, o desenvolvedor tem três tipos de políticas para atualizar cronogramas (TimelineReloadPolicy):
atEnd - atualizar após mostrar o último instantâneo
nunca - atualizar apenas no caso de uma chamada forçada
após (_ :) - atualizar após um determinado período de tempo.
No nosso caso, basta solicitar ao sistema um instantâneo até que os dados do cartão sejam atualizados no aplicativo 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]
}
Uma opção mais flexível seria útil se usar um algoritmo automático para selecionar os cartões dependendo do dia da semana e da hora.
Separadamente, vale ressaltar a exibição de um widget se ele estiver em uma pilha de widgets ( Smart Stack ). Nesse caso, podemos usar duas opções para gerenciar as prioridades: Sugestões de Siri ou definindo o valor de relevância de um TimelineEntry com o tipo TimelineEntryRelevance . TimelineEntryRelevance contém dois parâmetros:
score - a prioridade do instantâneo atual em relação a outros instantâneos;
duração - o tempo até que o widget permaneça relevante e o sistema possa colocá-lo na posição superior da pilha.
Ambos os métodos, bem como as opções de configuração para o widget, foram discutidos em detalhes na sessão WWDC .
Você também precisa falar sobre como manter a exibição de data e hora atualizada. Como não podemos atualizar regularmente o conteúdo do widget, vários estilos foram adicionados ao componente Texto. Ao usar um estilo, o sistema atualiza automaticamente o conteúdo do componente enquanto o widget está na tela. Talvez no futuro, a mesma abordagem seja estendida a outros componentes SwiftUI.
O texto suporta os seguintes estilos:
relativo- a diferença de tempo entre a data atual e a data especificada. É importante notar aqui: se a data for especificada no futuro, então a contagem regressiva começa, e depois disso a data a partir do momento em que chega a zero é mostrada. O mesmo comportamento será para os próximos dois estilos;
offset - semelhante ao anterior, mas com indicação em forma de prefixo com ±;
cronômetro - análogo a um cronômetro;
data - exibição de data ;
hora - exibição da hora .
Além disso, é possível exibir o intervalo de tempo entre as datas simplesmente especificando o 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)
}

Visualização do widget
Quando exibido pela primeira vez, o widget será aberto no modo de visualização, para isso precisamos retornar TimeLineEntry no método placeholder (em :). No nosso caso, é assim:
func placeholder(in context: Context) -> Self.Entry {
return CardListEntry(date: Date(), cards: testData)
}
Depois disso, o modificador redigido (motivo :) com o parâmetro de espaço reservado é aplicado à visualização . Nesse caso, os elementos do widget são exibidos desfocados.

Podemos remover esse efeito de alguns elementos usando o modificador unredacted () .
A documentação também diz que a chamada ao método placeholder (in :) é síncrona e o resultado deve retornar o mais rápido possível, ao contrário de getSnapshot (in: completed :) e getTimeline (in: completed :)
Elementos de arredondamento
Nas diretrizes, é recomendado combinar o arredondamento dos elementos com o arredondamento do widget; para isso , a estrutura ContainerRelativeShape foi adicionada no iOS 14 , que permite aplicar a forma de um contêiner a uma visualização.
.clipShape(ContainerRelativeShape())
Suporte Objective-C
Se você precisar adicionar código Objective-C ao widget (por exemplo, nós escrevemos a geração de imagens de código de barras nele), tudo acontece da maneira padrão adicionando o cabeçalho de ponte Objective-C. O único problema que encontramos foi que, durante a construção, o Xcode parou de ver arquivos de intent gerados automaticamente, então também os adicionamos ao cabeçalho de ponte :
#import "DynamicCardSelectionIntent.h"
#import "CardSelectionIntent.h"
#import "DynamicMultiSelectionIntent.h"
Tamanho do aplicativo
O teste foi realizado no Xcode 12 beta 6
Sem widget: 61,6 MB
Com widget: 62,2 MB Vou
resumir os principais pontos que foram discutidos no artigo:
- Os widgets são uma ótima maneira de sentir o SwiftUI na prática. Adicione-os ao seu projeto, mesmo que a versão mínima compatível seja inferior a iOS 14.
- WidgetBundle é usado para aumentar o número de widgets disponíveis, aqui está um ótimo exemplo de quantos widgets diferentes o ApolloReddit possui.
- IntentConfiguration ou StaticConfiguration ajudará a adicionar configurações personalizadas no próprio widget, se as configurações personalizadas não forem necessárias.
- Uma pasta compartilhada no sistema de arquivos nos grupos de aplicativos compartilhados ajudará você a sincronizar os dados com o aplicativo principal.
- O desenvolvedor recebe várias políticas para atualizar a linha do tempo (no final, nunca, depois (_ :)).
Sobre isso, o caminho espinhoso de desenvolver um widget em versões beta do Xcode pode ser considerado completo, resta apenas um passo simples - passar por uma revisão na App Store.
PS A versão com o widget passou por moderação e já está disponível para download na App Store!
Obrigado por ler até o fim, terei o maior prazer em sugestões e comentários. Por favor, responda a uma breve pesquisa para ver como os widgets são populares entre usuários e desenvolvedores.