Olá pessoal!
Meu nome é Dmitry. Por acaso, sou líder de uma equipe de 13 desenvolvedores de iOS nos últimos dois anos. E juntos estamos trabalhando no aplicativo Tinkoff Business .
Quero compartilhar com você nossa experiência em como liberar o aplicativo em um momento inesperado com o conjunto máximo de recursos ou correções de bugs e ainda não ficar cinza.
Vou falar sobre as práticas e abordagens que ajudaram a equipe a acelerar significativamente o desenvolvimento e os testes e a reduzir significativamente a quantidade de estresse, bugs, problemas com uma liberação não programada ou urgente. #MakeReleaseWithoutStress .
Vamos lá!
Descrição do problema
Imagine a seguinte situação.
Há outro lançamento. Foi precedido pelo teste de regressão; os testadores encontraram novamente um local onde, em vez de texto no aplicativo, o ID da linha é exibido.

Esse foi um dos problemas mais frequentes que encontramos.
Você pode não encontrar esse problema se o aplicativo não estiver localizado em outro idioma ou se toda a localização estiver escrita em linhas diretamente no código sem usar o arquivo Localizable.strings.
Mas você pode encontrar outros problemas que ajudaremos a resolver:
Razão → Efeito
Por que tudo isso está acontecendo?
Há um código de programa que é compilado. Se você escreveu algo errado (sintaticamente ou um nome de função incorreto ao chamar), seu projeto simplesmente não será montado. Isso é compreensível, óbvio e lógico.
Mas e as coisas como recursos?
Eles não são compilados, são simplesmente adicionados ao pacote depois que o código é compilado. Nesse sentido, um grande número de problemas pode ocorrer em tempo de execução, por exemplo, o caso descrito acima - com seqüências de caracteres na localização.
Procure uma solução
Pensamos em como esses problemas são resolvidos em geral e como podemos resolver isso. Lembrei-me de uma das conferências da Cocoaheads em mail.ru. Houve uma conversa sobre a comparação de ferramentas de geração de código.
Depois de ver novamente o que são essas ferramentas (bibliotecas / estruturas), finalmente encontramos o que era necessário.
Ao mesmo tempo, uma abordagem semelhante tem sido usada pelos desenvolvedores para Android há anos. O Google pensou neles e os tornou uma ferramenta pronta para uso. Mas a Apple, mesmo o Xcode estável, não pode nos fazer ...
Tudo o que restava era descobrir apenas qual instrumento escolher: Natalie , SwiftGen ou R.swift ?
Natalie não tinha suporte à localização, foi decidido abandoná-lo imediatamente. SwiftGen e R.swift tinham recursos muito semelhantes. Optamos por R.swift, simplesmente com base no número de estrelas, sabendo que a qualquer momento podemos mudar para SwiftGen.
Como a R.swift funciona
Um script de fase de compilação de pré-compilação é lançado, ele percorre a estrutura do projeto e gera um arquivo chamado R.generated.swift
, que precisará ser adicionado ao projeto (informaremos mais sobre como fazer isso no final).
O arquivo tem a seguinte estrutura:
import Foundation import Rswift import UIKit /// This `R` struct is generated and contains references to static resources. struct R: Rswift.Validatable { fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap(Locale.init) ?? Locale.current fileprivate static let hostingBundle = Bundle(for: R.Class.self) static func validate() throws { try intern.validate() } // ... /// This `R.string` struct is generated, and contains static references to 2 localization tables. struct string { /// This `R.string.localizable` struct is generated, and contains static references to 1196 localization keys. struct localizable { /// en translation: Apple Pay /// /// Locales: en, ru static let card_actions_activate_apple_pay = Rswift.StringResource(key: "card_actions_activate_apple_pay", tableName: "Localizable", bundle: R.hostingBundle, locales: ["en", "ru"], comment: nil) // ... /// en translation: Apple Pay /// /// Locales: en, ru static func card_actions_activate_apple_pay(_: Void = ()) -> String { return NSLocalizedString("card_actions_activate_apple_pay", bundle: R.hostingBundle, comment: "") } } } }
Uso:
let str = R.string.localizable.card_actions_activate_apple_pay() print(str) > Apple Pay
“Por Rswift.StringResource
precisamos do Rswift.StringResource
?”, Você pergunta. Eu mesmo não entendo por que gerá-lo, mas, como explicam os autores, é necessário para o seguinte: link .
Aplicação no mundo real
Uma pequena explicação do conteúdo abaixo:
* Foi - eles usaram a abordagem por um tempo, no final, eles a deixaram
* Tornou-se - a abordagem que usamos ao escrever um novo código
* Não era, mas você pode ter - uma abordagem que nunca existiu em nosso aplicativo, mas eu a conheci em vários projetos, naqueles tempos distantes, em que ainda não havia trabalhado no Tinkoff.ru.
Localização
Começamos a usar o R.swift
para localização, ele nos salvou dos problemas sobre os quais escrevemos desde o início. Agora, se o ID na localização foi alterado, o projeto não será montado.
* Isso só funciona se você alterar o ID em todas as localizações para outro. Se uma string permanecer em qualquer uma das localizações, na compilação, haverá um aviso de que esse ID não está localizado em todos os idiomas.

Não existe, mas você pode ter: final class NewsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() titleLabel.text = NSLocalizedString("news_title", comment: "News title") } }
Foi: extension String { public func localized(in bundle: Bundle = .main, value: String = "", comment: String = "") -> String { return NSLocalizedString(self, tableName: nil, bundle: bundle, value: value, comment: comment) } } final class NewsViewController: UIViewController { private enum Localized { static let newsTitle = "news_title".localized() } override func viewDidLoad() { super.viewDidLoad() titleLabel.text = Localized.newsTitle } }
Tornou-se: titleLabel.text = R.string.localizable.newsTitle()
Imagens
Agora, se renomearmos algo em * .xcassets e não mudarmos o código, o projeto simplesmente não será montado.
Foi: imageView.image = UIImage(named: "NotExist") // imageView.image = UIImage(named: "NotExist")! // crash imageView.image = #imageLiteral(resourceName: "NotExist") // crash
Tornou-se: imageView.image = R.image.tinkoffLogo() //
Storyboards
Foi: let someStoryboardName = "SomeStoryboard" // Change to something else (eg: "somestoryboard") - get nil or crash in else let someVCIdentifier = "SomeViewController" // Change to something else (eg: "someviewcontroller") - get nil or crash in else let storyboard = UIStoryboard(name: someStoryboardName, bundle: .main) let _vc = storyboard.instantiateViewController(withIdentifier: someVCIdentifier) guard let vc = _vc as? SomeViewController else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯}
Tornou-se: guard let vc = R.storyboard.someStoryboard.someViewController() else { // - , Fabric Firebase // fatalError() ¯\_(ツ)_/¯ }
E assim por diante
Storyboard de validação
R.validate () é uma ferramenta maravilhosa que bate em mãos (ou melhor, apenas lança um erro em um bloco de captura) se você fez algo errado nos arquivos storyboard ou xib.
Por exemplo:
- Indicou o nome da imagem, que não está no projeto
- Eles indicaram a fonte e depois pararam de usá-la e a excluíram do projeto (de info.plist)
Uso:
final class AppDelegate: UIResponder { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { #if DEBUG do { try R.validate() } catch { // fatalError // debug // - , production fatalError(error.localizedDescription) } #endif return true } }
E agora você está pronto para comprar dois!

Como implementar?
* Sistema baseado em componentes - um wiki , um conceito de desenvolvimento de código no qual os componentes (um conjunto de telas / módulos conectados juntos) são desenvolvidos em um ambiente fechado (no nosso caso, em pods locais), a fim de reduzir a coerência da base de código. Muitas pessoas conhecem a abordagem no back-end, que é baseada nesse conceito - microsserviços.
* Monolith - wiki , o conceito de desenvolvimento de código, no qual toda a base de código está em um repositório, e o código está intimamente relacionado. Este conceito é adequado para pequenos projetos com um conjunto finito de funções.
Se você estiver desenvolvendo um aplicativo monolítico ou usando apenas dependências de terceiros, estará com sorte (mas isso não é exato). Siga o tutorial e faça tudo estritamente sobre ele.
Este não foi o nosso caso. Nós nos envolvemos. Como usamos um sistema baseado em componentes,
incorporar o R.swift no aplicativo principal, decidimos incorporá-lo em pods locais (que são componentes).
Devido à constante atualização de localizações, imagens e todos os elementos que afetam o arquivo R.generated.swift, há muitos conflitos no arquivo gerado ao mesclar para a ramificação comum. E para evitar isso, você deve remover o R.generated.swift do repositório git. O autor também recomenda fazer isso .
Adicione as seguintes linhas ao .gitignore
.
# R.Swift generated files *.generated.swift
Além disso, se você não quiser gerar código para alguns recursos, sempre poderá ignorar arquivos individuais ou pastas inteiras:
"${PODS_ROOT}/R.swift/rswift" generate "${SRCROOT}/Example" "--rswiftignore" "Example/.rswiftignore"
descrição de .rswiftignore
Como no projeto principal, era importante não adicionar arquivos R.generated.swift de pods locais ao repositório git. Começamos a considerar opções de como isso pode ser feito:
- alias em R.generated.swift para que o arquivo (alias, por exemplo: R.swift) seja adicionado ao projeto e, ao compilar por referência, o arquivo real esteja disponível. Mas os cocoapods são inteligentes e não podem fazê-lo
- no podspec na fase de pré-compilação, adicione o arquivo R.generated.swift ao próprio projeto usando scripts, mas ele será adicionado simplesmente como um arquivo no sistema de arquivos, e o arquivo não aparecerá no projeto
- outras opções mais ou menos legais
magia em podfile
Magia
pre_install do |installer| installer.pod_targets.flat_map do |pod_target| if pod_target.pod_target_srcroot.include? 'LocalPods'
- e outra opção ... ainda adicione R.generated.swift ao git
Decidimos temporariamente a opção: "magia no Podfile", apesar do fato de ter várias deficiências:
- Ele só pôde ser iniciado a partir da raiz do projeto (embora os cocoapods possam ser iniciados em quase qualquer pasta do projeto)
- Todos os pods devem ter uma pasta chamada Fontes (embora isso não seja crítico se os pods estiverem em ordem)
- Ele era estranho e incompreensível, mas mais cedo ou mais tarde ele teria que apoiar (isso ainda é uma muleta)
- Se alguma biblioteca de terceiros estiver em uma pasta com "LocalPods" no caminho, ele tentará adicionar o arquivo R.generated.swift lá ou ocorrerá um erro fatal
prepare_command
Vivendo algum tempo com um roteiro e sofrendo, decidi estudar esse tópico mais amplamente e encontrei outra opção.
No Podspec, existe o comando prepare_com , destinado apenas à criação e modificação das fontes, que serão adicionadas ao projeto.
* Notícias - o nome do pod, que deve ser substituído pelo nome do seu pod local
* touch - comando para criar um arquivo. O argumento é o caminho relativo para o arquivo (incluindo o nome do arquivo com a extensão)
Em seguida, faremos fraudes com o News.podspec
Esse script é chamado a primeira vez que a pod install
executada e adiciona o arquivo necessário à pasta de origem na lareira.
Pod::Spec.new do |s|
A seguir, está outro "finta com ouvidos" - precisamos fazer uma chamada para o script R.swift para lareiras locais.
Pod::Spec.new do |s|
É verdade que existe um "mas". prepare_command
não funciona com pods locais, ou melhor, mas em alguns casos especiais. Há uma discussão sobre este tópico no Github .
Fatalidade
* Fatality - wiki , o sucesso final em Mortal Kombat.
Depois de um pouco mais de pesquisa, encontrei outra solução - um híbrido de abordagens c prepare_command
e pre_install
.
Uma pequena modificação da magia do Podfile:
pre_install do |installer|
E o mesmo script que não foi executado para lareiras locais
Pod::Spec.new do |s|
No final, funciona como esperamos.
Finalmente!
PS:
Tentei fazer outro comando personalizado em vez de prepare_command
, mas o pod lib lint
(um comando para validar o conteúdo do podspec e o próprio forno) jura em variáveis extras e não passa.
Lareiras não locais
Nos pods remotos (aqueles que estão em seu próprio repositório), você não precisa de toda essa mágica de script, descrita acima, porque a base de códigos está estritamente vinculada à versão da dependência.
Basta simplesmente incorporar o script R.swift de Example (um projeto gerado após o comando pod lib create <Name>) e adicionar R.generated.swift ao pacote da biblioteca (abaixo). Se o projeto não tiver Exemplo, você precisará escrever scripts que serão semelhantes aos que eu citei.
PS:
Há um pequeno esclarecimento:
R.swift + Xcode 10 + novo sistema de construção + construção incremental! = <3
Mais informações sobre o problema na página principal da biblioteca ou aqui
R.swift v4.0.0 não funciona com cocoapods 1.6.0 :(
Eu acho que em breve todos os problemas serão corrigidos.
Conclusão
Você deve sempre manter a barra de qualidade o mais alta possível. Isso é especialmente importante para aplicativos que trabalham com finanças.
Nesse caso, você não precisa sobrecarregar o teste e encontrar erros o mais cedo possível. No nosso caso, é no momento em que o desenvolvedor compilou o código ou na execução de teste para solicitações de recebimento. Portanto, encontramos a falta de localização não pelo olhar atento dos testadores ou por testes automatizados, mas pelo processo usual de criação do aplicativo.
Você também precisa considerar o fato de que essa é uma ferramenta de terceiros vinculada à estrutura do projeto e analisa seu conteúdo. Se a estrutura do arquivo do projeto for alterada, a ferramenta precisará ser alterada.
Assumimos esse risco e, nesse caso, estamos sempre prontos para alterar essa ferramenta para outra ou escrever sua própria.
E o ganho da R.swift é um grande número de horas de trabalho que a equipe pode gastar em coisas muito mais importantes: novos recursos, novas soluções técnicas, melhoria da qualidade e assim por diante. A R.swift retornou totalmente a quantidade de tempo gasto em sua integração, levando em consideração a possível substituição no futuro por outra solução semelhante.
R.swift
Bônus
Você pode brincar com um exemplo para ver imediatamente com seus próprios olhos o lucro da geração de código de recursos. O código fonte do projeto "para brincar": GitHub .
Muito obrigado por ler o artigo ou apenas folhear este lugar, estou satisfeito em qualquer caso)
Só isso.