Controle de Recursos. Personalizar SwiftGen

Provavelmente, em todos os grandes projetos do iOS, o de longa duração pode se deparar com ícones que não são usados ​​em lugar algum, ou acessar chaves de localização que há muito se foram. Na maioria das vezes, essas situações surgem da desatenção e a automação é a melhor cura para a desatenção.


Na equipe iOS do HeadHunter, prestamos muita atenção na automatização das tarefas rotineiras que um desenvolvedor pode encontrar. Com este artigo, queremos iniciar um ciclo de histórias sobre essas ferramentas e abordagens que simplificam nosso trabalho diário.


Algum tempo atrás, conseguimos controlar os recursos do aplicativo usando o utilitário SwiftGen. Sobre como configurá-lo, como viver com ele e como esse utilitário ajuda a mudar a verificação da relevância dos recursos para o compilador, e falaremos sobre o gato.



SwiftGen é um utilitário que permite gerar código Swift para acessar vários recursos de um projeto Xcode, entre eles:


  • fontes
  • cores
  • storyboards;
  • cadeias de localização;
  • ativos.

Todos poderiam escrever um código semelhante para inicializar imagens ou cadeias de localização:


logoImageView.image = UIImage(named: "Swift") nameLabel.text = String( format: NSLocalizedString("languages.swift.name", comment: ""), locale: Locale.current ) 

Para indicar o nome da imagem ou chave de localização, usamos literais de string. O que está escrito entre aspas duplas não é validado pelo compilador ou pelo ambiente de desenvolvimento (Xcode). Aí reside o seguinte conjunto de problemas:


  • Você pode fazer um erro de digitação;
  • você pode esquecer de atualizar o uso no código após editar ou excluir a chave / imagem.

Vamos ver como podemos melhorar esse código com o SwiftGen.


Para nossa equipe, a geração foi relevante apenas para cadeias e ativos, e eles serão discutidos no artigo. A geração para outros tipos de recursos é semelhante e, se desejado, é facilmente dominada de forma independente.


Implementação do Projeto


Primeiro você precisa instalar o SwiftGen. Optamos por instalá-lo através do CocoaPods como uma maneira conveniente de distribuir o utilitário entre todos os membros da equipe. Mas isso pode ser feito de outras maneiras, descritas em detalhes na documentação . No nosso caso, tudo o que precisa ser feito é adicionar o pod 'SwiftGen' e, em seguida, adicionar uma nova fase de build ( Build Phase ) que iniciará o SwiftGen antes de compilar o projeto.


 "$PODS_ROOT"/SwiftGen/bin/swiftgen 

É importante executar o SwiftGen antes de iniciar a fase Compile Sources para evitar erros ao compilar o projeto.



Agora estamos prontos para adaptar o SwiftGen ao nosso projeto.


Configurar SwiftGen


Primeiro de tudo, você precisa configurar os modelos pelos quais o código para acessar os recursos será gerado. O utilitário já contém um conjunto de modelos para gerar código, todos eles podem ser visualizados no github e, em princípio, estão prontos para uso. Os modelos são escritos na linguagem Stencil , você pode estar familiarizado com ela se usou Sourcery ou tocou com Kitura . Se desejado, cada um dos modelos pode ser adaptado aos seus próprios guias.
Por exemplo, considere o modelo que a enum gera para acessar cadeias de caracteres de localização. Pareceu-nos que no padrão há muito supérfluo e pode ser simplificado. Um exemplo simplificado com comentários explicativos está sob o spoiler.


Exemplo de modelo
 {#      #} {% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %} {#    #} {% macro parametersBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} _ p{{forloop.counter}}: {{type}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {% macro argumentsBlock types %}{% filter removeNewlines:"leading" %} {% for type in types %} p{{forloop.counter}}{% if not forloop.last %}, {% endif %} {% endfor %} {% endfilter %}{% endmacro %} {#       enum        #} {% macro recursiveBlock table item sp %} {{sp}}{% for string in item.strings %} {{sp}}{% if not param.noComments %} {{sp}}/// {{string.translation}} {{sp}}{% endif %} {{sp}}{% if string.types %} {{sp}}{{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String { {{sp}} return localize("{{string.key}}", {% call argumentsBlock string.types %}) {{sp}}} {{sp}}{% else %} {{sp}}{{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = localize("{{string.key}}") {{sp}}{% endif %} {{sp}}{% endfor %} {{sp}}{% for child in item.children %} {{sp}}{{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {{sp}}{% set sp2 %}{{sp}} {% endset %} {{sp}}{% call recursiveBlock table child sp2 %} {{sp}}} {{sp}}{% endfor %} {% endmacro %} import Foundation {#   enum #} {% set enumName %}{{param.enumName|default:"L10n"}}{% endset %} {{accessModifier}} enum {{enumName}} { {% if tables.count > 1 %} {% for table in tables %} {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} { {% call recursiveBlock table.name table.levels " " %} } {% endfor %} {% else %} {% call recursiveBlock tables.first.name tables.first.levels " " %} {% endif %} } {#  enum Localization         #} extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } } 

É conveniente salvar o próprio arquivo de modelo na raiz do projeto, por exemplo, na pasta SwiftGen/Templates para que esse modelo esteja disponível para todos que trabalham no projeto.
O utilitário suporta a configuração por meio do arquivo YAML swiftgen.yml , no qual você pode especificar os caminhos para os arquivos de origem, modelos e parâmetros adicionais. Crie-o na raiz do projeto na pasta Swiftgen e, posteriormente, agrupe outros arquivos relacionados ao script na mesma pasta.
Para o nosso projeto, esse arquivo pode ser assim:


 xcassets: - paths: ../SwiftGenExample/Assets.xcassets templatePath: Templates/ImageAssets.stencil output: ../SwiftGenExample/Image.swift params: enumName: Image publicAccess: 1 noAllValues: 1 strings: - paths: ../SwiftGenExample/en.lproj/Localizable.strings templatePath: Templates/LocalizableStrings.stencil output: ../SwiftGenExample/Localization.swift params: enumName: Localization publicAccess: 1 noComments: 0 

De fato, os caminhos para arquivos e modelos são indicados lá, bem como parâmetros adicionais que são passados ​​para o contexto do modelo.
Como o arquivo não está na raiz do projeto, precisamos especificar o caminho para ele ao iniciar o Swiftgen. Altere nosso script de inicialização:


 "$PODS_ROOT"/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml 

Agora nosso projeto pode ser montado. Após a montagem, dois arquivos Localization.swift e Image.swift devem aparecer na pasta do projeto nos caminhos especificados em Image.swift . Eles precisam ser adicionados ao projeto Xcode. No nosso caso, os arquivos gerados contêm o seguinte:


Para strings:
 public enum Localization { public enum Languages { public enum ObjectiveC { /// General-purpose, object-oriented programming language that adds Smalltalk-style messaging to the C programming language public static let description = localize("languages.objective-c.description") /// https://en.wikipedia.org/wiki/Objective-C public static let link = localize("languages.objective-c.link") /// Objective-C public static let name = localize("languages.objective-c.name") } public enum Swift { /// General-purpose, multi-paradigm, compiled programming language developed by Apple Inc. for iOS, macOS, watchOS, tvOS, and Linux public static let description = localize("languages.swift.description") /// https://en.wikipedia.org/wiki/Swift_(programming_language) public static let link = localize("languages.swift.link") /// Swift public static let name = localize("languages.swift.name") } } public enum MainScreen { /// Language public static let title = localize("main-screen.title") public enum Button { /// View in Wikipedia public static let title = localize("main-screen.button.title") } } } extension Localization { fileprivate static func localize(_ key: String, _ args: CVarArg...) -> String { return String( format: NSLocalizedString(key, comment: ""), locale: Locale.current, arguments: args ) } } 

Para imagens:
 public enum Image { public enum Logos { public static var objectiveC: UIImage { return image(named: "ObjectiveC") } public static var swift: UIImage { return image(named: "Swift") } } private static func image(named name: String) -> UIImage { let bundle = Bundle(for: BundleToken.self) guard let image = UIImage(named: name, in: bundle, compatibleWith: nil) else { fatalError("Unable to load image named \(name).") } return image } } private final class BundleToken {} 

Agora você pode substituir todos os usos de cadeias de localização e inicialização de imagens no formato UIImage(named: "") pelo que geramos. Isso facilitará o rastreamento de alterações ou a remoção de chaves de cadeia de localização. Em qualquer um desses casos, o projeto simplesmente não será montado até que todos os erros associados às alterações tenham sido corrigidos.
Após as alterações, nosso código fica assim:


  let logos = Image.Logos.self let localization = Localization.self private func setupWithLanguage(_ language: ProgrammingLanguage) { switch language { case .Swift: logoImageView.image = logos.swift nameLabel.text = localization.Languages.Swift.name descriptionLabel.text = localization.Languages.Swift.description wikiUrl = localization.Languages.Swift.link.toURL() case .ObjectiveC: logoImageView.image = logos.objectiveC nameLabel.text = localization.Languages.ObjectiveC.name descriptionLabel.text = localization.Languages.ObjectiveC.description wikiUrl = localization.Languages.ObjectiveC.link.toURL() } } 

Configurando um projeto no Xcode


Há um problema com os arquivos gerados: eles podem ser alterados manualmente por engano e, como são substituídos do zero a cada compilação, essas alterações podem ser perdidas. Para evitar isso, você pode bloquear os arquivos para gravação depois de SwiftGen script SwiftGen .
Isso pode ser alcançado usando o chmod . Reescrevemos nossa Build Phase com o lançamento do SwiftGen da seguinte forma:


 PODSROOT="$1" OUTPUT_FILES=() COUNTER=0 while [ $COUNTER -lt ${SCRIPT_OUTPUT_FILE_COUNT} ]; do tmp="SCRIPT_OUTPUT_FILE_$COUNTER" OUTPUT_FILES+=(${!tmp}) COUNTER=$[$COUNTER+1] done for file in "${OUTPUT_FILES[@]}" do if [ -f $file ] then chmod a=rw "$file" fi done $PODSROOT/SwiftGen/bin/swiftgen config run --config SwiftGen/swiftgen.yml for file in "${OUTPUT_FILES[@]}" do chmod a=r "$file" done 

O script é bem simples. Antes de iniciar a geração, se os arquivos existirem, concedemos a eles permissões de gravação. Depois de executar o script, bloqueamos a capacidade de modificar arquivos.
Para facilitar a edição e a verificação do script para revisão, é conveniente colocá-lo em um arquivo runswiftgen.sh separado. A versão final do script com pequenas modificações pode ser encontrada aqui . Agora, a nossa Build Phase ficará assim: passamos o caminho para a pasta Pods para a entrada do script:


 "$SRCROOT"/SwiftGen/runswiftgen.sh "$PODS_ROOT" 

Nós reconstruímos o projeto e, agora, quando você tenta modificar manualmente o arquivo gerado, um aviso é exibido:



Portanto, a pasta com Swiftgen agora contém um arquivo de configuração, um script para bloquear arquivos e iniciar o Swiftgen e uma pasta com modelos personalizados. É conveniente adicioná-lo ao projeto para edição adicional, se necessário.



E como os Image.swift Localization.swift e Image.swift gerados automaticamente, você pode adicioná-los ao .gitignore para não precisar resolver conflitos após a git merge novamente.


Também gostaria de chamar a atenção para a necessidade de especificar arquivos de entrada / saída para a construção da fase SwiftGen se o seu projeto usar o novo sistema de compilação do Xcode (é o sistema de compilação padrão do Xcode 10). Na lista Arquivos de entrada, você deve especificar todos os arquivos com base nos quais o código é gerado. No nosso caso, este é o próprio script, config, templates, .strings e arquivos .xcassets:


 $(SRCROOT)/SwiftGen/runswiftgen.sh $(SRCROOT)/SwiftGen/swiftgen.yml $(SRCROOT)/SwiftGen/Templates/ImageAssets.stencil $(SRCROOT)/SwiftGen/Templates/LocalizableStrings.stencil $(SRCROOT)/SwiftGenExample/en.lproj/Localizable.strings $(SRCROOT)/SwiftGenExample/Assets.xcassets 

Nos arquivos de saída, colocamos os arquivos nos quais o SwiftGen gera o código:


 $(SRCROOT)/SwiftGenExample/Image.swift $(SRCROOT)/SwiftGenExample/Localization.swift 

A indicação desses arquivos é necessária para que o sistema de construção possa decidir se deseja executar o script, dependendo da presença ou ausência de alterações nos arquivos e em que momento da montagem do projeto esse script deve ser executado.


Sumário


O SwiftGen é uma boa ferramenta para proteger contra nossos descuidos ao trabalhar com os recursos do projeto. Com isso, conseguimos gerar automaticamente código para acessar os recursos do aplicativo e mudar parte do trabalho para verificar a relevância dos recursos para o compilador, o que significa simplificar um pouco o trabalho. Além disso, configuramos o projeto Xcode para que mais trabalho com a ferramenta seja mais conveniente.


Prós:


  1. Mais fácil de controlar os recursos do projeto.
  2. Os erros de digitação são reduzidos, torna-se possível usar a substituição automática.
  3. Os erros são verificados no estágio de compilação.

Contras:


  1. Não há suporte para Localizable.stringsdict.
  2. Os recursos que não são usados ​​não são levados em consideração.

Exemplo completo pode ser visto no github

Source: https://habr.com/ru/post/pt423381/


All Articles