Gerenciando Configurações de Construção em Xcode
O Manual Ausente
para Desenvolvimento Rápido
O Guia que Gostaria de ter Quando Comecei
Join 20,000+ Developers Learning About Swift Development
Download Your Free Copy
Um novo projecto Xcode define duas configurações de compilação, Debug e Release. A maioria dos projetos define uma ou mais configurações de compilação adicionais por vários motivos. Isso não é novo e é uma boa prática usar configurações de build para adaptar um build às necessidades específicas do ambiente para o qual ele será implantado.
Neste episódio, eu mostro como gerenciar com segurança dados específicos para uma configuração de build, como chaves de API, credenciais e outros dados sensíveis. Existem várias estratégias para gerenciar configurações de build, mas existem diferenças importantes que você precisa considerar, especialmente no contexto da segurança.
Adicionando uma Configuração
Código X e crie um novo projeto escolhendo o modelo Single View App do iOS > Seção de Aplicações.
Denomine as Configurações do projeto, defina Language para Swift, e certifique-se de que as caixas de seleção na parte inferior estejam desmarcadas. Diga ao Xcode onde você gostaria de armazenar o projeto e clique em Create.
Abra o Project Navigator à esquerda e clique no projeto na parte superior. Selecione o projeto na seção Project (Projeto) para mostrar os detalhes do projeto. Um novíssimo projeto Xcode define duas configurações de build, Debug e Release.
Você normalmente usa a configuração Debug durante o desenvolvimento enquanto a configuração Release é usada para criar builds App Store ou TestFlight. Isto provavelmente não é novidade para você.
Muitas aplicações comunicam com um backend e é uma prática comum ter um ambiente de encenação e produção. O ambiente de encenação é usado durante o desenvolvimento. Clique duas vezes na configuração de Debug e renomeie-a para Staging.
Saiba adicionar uma terceira configuração para o ambiente de produção. Clique no botão + na parte inferior da tabela, escolha Duplicar Configuração “Staging” e nomeie a configuração Produção.
Criemos um esquema para cada configuração para facilitar a troca rápida entre ambientes. Seleccione o esquema no topo e escolha Manage Schemes… a partir do menu. Selecione o esquema chamado Configurações e clique nele mais uma vez. Renomeie-o para Staging.
Com o esquema selecionado, clique no ícone da engrenagem na parte inferior e escolha Duplicate. Nomeie o esquema Produção. Selecione Run à esquerda e defina Build Configuration como Production.
É isso aí. Agora temos uma configuração de build para a encenação e produção. Os esquemas fazem com que seja rápido e fácil alternar entre configurações de build.
Configurações de Build definidas pelo usuário
Como mencionei anteriormente, existem várias soluções para gerenciar dados específicos de uma configuração de build em particular. Neste episódio, eu mostro uma solução que uso em qualquer projeto que tenha alguma complexidade. Antes de apresentar a solução que tenho em mente, gostaria de mostrar outra solução que é frequentemente utilizada pelos desenvolvedores.
Selecionar o projeto no Project Navigator à esquerda. Selecione o alvo Configurações na seção Alvos e clique na aba Configurações de Compilação no topo.
A aba Configurações de Compilação mostra as configurações de compilação para o alvo Configurações. É possível expandir esta lista com as Configurações de Compilação que você definir. Clique no botão + na parte superior e escolha Add User-Defined Setting.
Nome a configuração definida pelo usuário BASE_URL. Definir uma URL base é comum para aplicações que interagem com um backend.
Qual é a vantagem de definir uma configuração definida pelo usuário? A configuração definida pelo usuário nos permite definir um valor para BASE_URL para cada configuração de build. Clique no triângulo à esquerda da configuração definida pelo usuário para mostrar a lista de configurações de build.
Configurar o valor para Produção e Release para https://cocoacasts.com e o valor para Staging para https://staging.cocoacasts.com.
Como o nome implica, uma configuração de build está disponível durante o processo de build. Seu código não pode acessar diretamente as configurações de compilação que você definir. Este é um equívoco comum. Há uma solução, no entanto. Abra Info.plist e adicione um novo par chave/valor. Defina a chave para BASE_URL e o valor para $(BASE_URL)
.
Qual é o valor de adicionar um par chave/valor ao Info.plist do projeto? O valor do par chave/valor é atualizado no momento da construção. Xcode inspeciona as configurações de construção para a configuração de construção atual e define o valor da chave BASE_URL no Info.plist.
Vamos experimentá-lo. Abra AppDelegate.swift e navegue para o método application(_:didFinishLaunchingWithOptions:)
. Precisamos acessar o conteúdo do arquivo Info.plist. Isto é possível através da propriedade infoDictionary
da classe Bundle
. O pacote em que estamos interessados é o pacote principal. Como o nome implica, a propriedade infoDictionary
é um dicionário de pares chave/valor e acessamos o valor para a BASE_URL
key.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Bundle.main.infoDictionary?) return true}
Selecionar o esquema de produção no topo e executar a aplicação no simulador. Inspeccione a saída no console.
Optional(https://staging.cocoacasts.com)
Selecione o esquema de produção na parte superior e execute a aplicação no simulador. Inspeccione a saída na consola. Isso é muito bom. Certo?
Optional(https://staging.cocoacasts.com)
Uma palavra sobre segurança
Os desenvolvedores acham esta solução muito conveniente. E é. Há um problema, no entanto. É fácil extrair o arquivo Info.plist de aplicativos baixados da App Store. O que acontece se você armazenar chaves de API, credenciais ou outras informações confidenciais no arquivo Info.plist? Isto introduz um risco de segurança significativo que podemos e devemos evitar.
I’d like to show you a simple solution that offers the same flexibility, type safety, and improved security. Crie um novo arquivo Swift e nomee-o Configuration.swift.
Definir um enum com o nome Configuration e um valor bruto do tipo String
. Definimos um caso para cada configuração de build, staging
, production
, e release
.
import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release}
Antes de continuarmos com a implementação do Enum de Configuração, precisamos atualizar o arquivo Info.plist. Abra o Info.plist e adicione um par chave/valor. A chave é Configuração e o valor é $(CONFIGURATION)
. O valor de CONFIGURAÇÃO é automaticamente definido para nós. Nós não precisamos de nos preocupar com isso. Como o nome implica, o valor é igual ao nome da configuração do build com o qual o build é criado.
Revisit Configuration.swift. Nós queremos acesso fácil à configuração do build, o valor armazenado no arquivo Info.plist do projeto. Defina uma propriedade estática, constante, current
, do tipo Configuration
. Nós acessamos o valor que é armazenado no arquivo Info.plist para a configuração da chave e o lançamos a uma instância String
. Lançamos um erro fatal se isso falhar porque isso nunca deveria acontecer.
import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release // MARK: - Current Configuration static let current: Configuration = { guard let rawValue = Bundle.main.infoDictionary? as? String else { fatalError("No Configuration Found") } }()}
Usamos o valor do arquivo Info.plist para criar uma instância Configuration
. Nós lançamos outro erro fatal se a inicialização falhar. Note que nós rebaixamos o valor armazenado na lima do Info.plist. A instância Configuration
é devolvida a partir do fechamento. Não se esqueça de anexar um par de parênteses ao fechamento.
import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release // MARK: - Current Configuration static let current: Configuration = { guard let rawValue = Bundle.main.infoDictionary? as? String else { fatalError("No Configuration Found") } guard let configuration = Configuration(rawValue: rawValue.lowercased()) else { fatalError("Invalid Configuration") } return configuration }()}
Vamos experimentar. Abra AppDelegate.swift e imprima a configuração atual. Selecione o Staging scheme, execute a aplicação, e inspecione a saída no console.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.current) return true}
staging
Configuração de extensão
Você provavelmente sabe que eu gosto de aproveitar os enums para criar namespaces. Deixe-me mostrar-lhe como podemos melhorar a implementação actual do Configuration
enum. A solução mais simples é definir uma propriedade estática, computada, baseURL
, do tipo URL
. Você também pode definir a propriedade como uma propriedade estática, constante. Esta é uma escolha pessoal. Utilizamos uma declaração switch
para retornar a URL base para cada configuração de build.
import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release // MARK: - Current Configuration static let current: Configuration = { guard let rawValue = Bundle.main.infoDictionary? as? String else { fatalError("No Configuration Found") } guard let configuration = Configuration(rawValue: rawValue.lowercased()) else { fatalError("Invalid Configuration") } return configuration }() // MARK: - Base URL static var baseURL: URL { switch current { case .staging: return URL(string: "https://staging.cocoacasts.com")! case .production, .release: return URL(string: "https://cocoacasts.com")! } }}
Existem vários detalhes que gostaria de salientar. Primeiro, eu não uso um caso default
. Estou explícito sobre o valor que é retornado para cada configuração de build. Isto facilita a detecção de problemas e torna o código mais legível e intuitivo. Segundo, eu uso o ponto de exclamação para forçar o desembrulhamento do valor retornado pelo inicializador da estrutura URL
. Este é um dos raros cenários em que eu utilizo o ponto de exclamação para forçar o desembrulhamento de um valor. É conveniente, mas, mais importante, a URL base nunca deve ser igual a nil
.
Open AppDelegate.swift e imprimir o valor da propriedade baseURL
computed. Com o esquema definido para o Staging, execute a aplicação e inspecione a saída no console.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.baseURL) return true}
https://staging.cocoacasts.com
Este padrão tem algumas vantagens sobre a primeira solução que implementamos. Nós aproveitamos a segurança do tipo Swift e não estamos mais lidando com um valor opcional. Também podemos nos beneficiar do auto-completamento do Xcode. Essas vantagens são sutis, mas você passa a apreciá-las com o tempo.
Vamos colocar a cereja no bolo adicionando namespaces à mistura. Crie um novo arquivo Swift e nomeie-o como Configuration+DarkSky.swift.
Criar uma extensão para o enum de Configuração e definir um enum com nome DarkSky
na extensão. Dark Sky é um serviço meteorológico que eu uso de tempos em tempos.
import Foundationextension Configuration { enum DarkSky { }}
> O enum definido como DarkSky
define uma propriedade estática, constante, apiKey
, do tipo String
. Nós ligamos a configuração atual e retornamos um valor diferente para cada configuração de construção. Como mencionei anteriormente, você também pode declarar a propriedade como uma propriedade estática, variável. Isso cabe a você decidir.
import Foundationextension Configuration { enum DarkSky { static let apiKey: String = { switch Configuration.current { case .staging: return "123" case .production: return "456" case .release: return "789" } }() }}
Existem várias vantagens para esta abordagem. A configuração para o Dark Sky API é bem espaçada. Isso também facilita colocar a configuração para a API do Céu Escuro em um arquivo separado.
AppDelegate.swift e imprimir a chave da API para o serviço de tempo do Céu Escuro. Com o esquema definido para Staging, execute o aplicativo e inspecione a saída no console.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.DarkSky.apiKey) return true}
123
What’s Next?
Este padrão adiciona conveniência, tipo de segurança, e segurança aos seus projetos. É fácil de adotar e, uma vez implementado, é simples de estender. É um padrão que eu gosto de usar por uma série de razões. Eu recomendo vivamente que experimente.