IOS 14 Widgets - Recursos e limitações





Este ano, existem várias oportunidades interessantes para os desenvolvedores de iOS drenar a bateria do iPhone para melhorar a experiência do usuário, uma das quais são novos widgets. Enquanto todos aguardamos o lançamento da versão do sistema operacional, gostaria de compartilhar minha experiência ao escrever um widget para o aplicativo "Wallet" e dizer quais oportunidades e limitações nossa equipe encontrou nas versões beta do Xcode.



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):

  1. Foque o widget em uma ideia e problema, não tente repetir todas as funcionalidades do aplicativo.
  2. Exiba mais informações dependendo do tamanho, em vez de apenas dimensionar o conteúdo.
  3. 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.
  4. 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:

  1. Permitir que o usuário escolha os cartões. Quem, senão ele, sabe quais são as cartas mais importantes!
  2. Mostra os últimos mapas usados.
  3. 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:

  1. Chamando o WidgetCenter.shared.reloadAllTimelines () usado anteriormente
  2. Quando um usuário adiciona um widget à área de trabalho
  3. 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:

  1. 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.
  2. WidgetBundle é usado para aumentar o número de widgets disponíveis, aqui está um ótimo exemplo de quantos widgets diferentes o ApolloReddit possui.
  3. IntentConfiguration ou StaticConfiguration ajudará a adicionar configurações personalizadas no próprio widget, se as configurações personalizadas não forem necessárias.
  4. Uma pasta compartilhada no sistema de arquivos nos grupos de aplicativos compartilhados ajudará você a sincronizar os dados com o aplicativo principal.
  5. 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.



All Articles