Articles

Gestión de configuraciones de compilación en Xcode

Recursos

Descargue su copia gratuita de
El manual que falta
para el desarrollo de Swift

La guía que desearía haber tenido cuando empecé

Únase a 20,000+ Desarrolladores que aprenden sobre el desarrollo de Swift

Descargue su copia gratuita

Un nuevo proyecto de Xcode define dos configuraciones de compilación, Debug y Release. La mayoría de los proyectos definen una o más configuraciones de compilación adicionales por diversas razones. Esto no es nuevo y es una buena práctica utilizar las configuraciones de compilación para adaptar una compilación a las necesidades específicas del entorno en el que se va a desplegar.

En este episodio, te muestro cómo gestionar de forma segura los datos que son específicos para una configuración de compilación, tales como claves de la API, credenciales y otros datos sensibles. Hay varias estrategias para gestionar las configuraciones de compilación, pero hay diferencias importantes que debes tener en cuenta, especialmente en el contexto de la seguridad.

Añadir una configuración

Enciende Xcode y crea un nuevo proyecto eligiendo la plantilla Single View App de la sección de aplicaciones iOS >.

Setting Up the Project

Nombra el proyecto Configurations, establece Language a Swift, y asegúrate de que las casillas de verificación en la parte inferior están desmarcadas. Dígale a Xcode dónde le gustaría almacenar el proyecto y haga clic en Crear.

Setting Up the Project

Abra el navegador de proyectos a la izquierda y haga clic en el proyecto en la parte superior. Seleccione el proyecto en la sección Proyecto para mostrar los detalles del proyecto. Un nuevo proyecto de Xcode define dos configuraciones de compilación, Debug y Release.

Build Configurations in Xcode

Usted suele utilizar la configuración Debug durante el desarrollo, mientras que la configuración Release se utiliza para crear compilaciones de App Store o TestFlight. Esto probablemente no es nuevo para usted.

Muchas aplicaciones se comunican con un backend y es una práctica común tener un entorno de staging y otro de producción. El entorno de staging se utiliza durante el desarrollo. Haz doble clic en la configuración de depuración y renómbrala a Staging.

Build Configurations in Xcode

Añadamos una tercera configuración para el entorno de producción. Haga clic en el botón + en la parte inferior de la tabla, elija Duplicar configuración de «Puesta en marcha» y nombre la configuración Producción.

Duplicating a Build Configuration

Duplicating a Build Configuration

Creemos un esquema para cada configuración para facilitar el cambio rápido entre entornos. Seleccione el esquema en la parte superior y elija Gestionar esquemas… en el menú. Seleccione el esquema llamado Configuraciones y haga clic en él una vez más. Cámbiele el nombre a Staging.

Update Schemes

Con el esquema seleccionado, haga clic en el icono del engranaje en la parte inferior y elija Duplicate. Nombra el esquema como Producción. Seleccione Ejecutar a la izquierda y establezca Configuración de compilación en Producción.

Update Schemes

Eso es todo. Ahora tenemos una configuración de construcción para la puesta en escena y la producción. Los esquemas hacen que sea rápido y fácil cambiar entre las configuraciones de compilación.

Configuración de compilación definida por el usuario

Como he mencionado anteriormente, hay varias soluciones para gestionar los datos que son específicos de una configuración de compilación particular. En este episodio, muestro una solución que utilizo en cualquier proyecto que tenga cierta complejidad. Antes de exponer la solución que tengo en mente, me gustaría mostrarte otra solución que suelen utilizar los desarrolladores.

Elige el proyecto en el navegador de proyectos de la izquierda. Selecciona el objetivo Configuraciones de la sección Objetivos y haz clic en la pestaña Ajustes de construcción en la parte superior.

Target Build Settings

La pestaña Ajustes de construcción muestra los ajustes de construcción para el objetivo Configuraciones. Es posible ampliar esta lista con los ajustes de compilación que usted defina. Haga clic en el botón + en la parte superior y elija Agregar configuración definida por el usuario.

Adding a User-Defined Setting

Nombre la configuración definida por el usuario BASE_URL. Definir una URL base es común para las aplicaciones que interactúan con un backend.

Adding a User-Defined Setting

¿Cuál es el beneficio de definir una configuración definida por el usuario? La configuración definida por el usuario nos permite establecer un valor para BASE_URL para cada configuración de construcción. Haga clic en el triángulo a la izquierda de la configuración definida por el usuario para mostrar la lista de configuraciones de compilación.

Adding a User-Defined Setting

Configure el valor para Production y Release en https://cocoacasts.com y el valor para Staging en https://staging.cocoacasts.com.

Adding a User-Defined Setting

Como su nombre indica, una configuración de compilación está disponible durante el proceso de compilación. Su código no puede acceder directamente a los ajustes de compilación que defina. Este es un error común. Sin embargo, hay una solución. Abra Info.plist y añada un nuevo par clave/valor. Establezca la clave como BASE_URL y el valor como $(BASE_URL).

Updating Info.plist

¿Qué valor tiene añadir un par clave/valor al Info.plist del proyecto? El valor del par clave/valor se actualiza en tiempo de compilación. Xcode inspecciona los ajustes de construcción para la configuración de construcción actual y establece el valor de la clave BASE_URL en el Info.plist del proyecto.

Vamos a probarlo. Abre AppDelegate.swift y navega hasta el método application(_:didFinishLaunchingWithOptions:). Necesitamos acceder al contenido del archivo Info.plist. Esto es posible a través de la propiedad infoDictionary de la clase Bundle. El bundle que nos interesa es el principal. Como su nombre indica, la propiedad infoDictionary es un diccionario de pares clave/valor y accedemos al valor de la clave BASE_URL.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Bundle.main.infoDictionary?) return true}

Seleccione el esquema de Producción en la parte superior y ejecute la aplicación en el simulador. Inspeccione la salida en la consola.

Optional(https://staging.cocoacasts.com)

Seleccione el esquema Staging en la parte superior y ejecute la aplicación en el simulador. Inspeccione la salida en la consola. Eso es muy bonito. ¿Verdad?

Optional(https://staging.cocoacasts.com)

Unas palabras sobre la seguridad

La mayoría de los desarrolladores encuentran esta solución muy conveniente. Y lo es. Sin embargo, hay un problema. Es fácil extraer el archivo Info.plist de las aplicaciones descargadas de la App Store. ¿Qué sucede si almacena claves de API, credenciales u otra información sensible en el archivo Info.plist? Esto introduce un riesgo de seguridad importante que podemos y debemos evitar.

Me gustaría mostrarte una solución sencilla que ofrece la misma flexibilidad, seguridad de tipo y seguridad mejorada. Crea un nuevo archivo Swift y nómbralo Configuration.swift.

Creating a Swift File

Creating a Swift File

Define un enum con nombre Configuration y un valor raw de tipo String. Definimos un caso para cada configuración de construcción, staging, production y release.

import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release}

Antes de continuar con la implementación del enum Configuración, necesitamos actualizar el archivo Info.plist. Abre Info.plist y añade un par clave/valor. La clave es Configuración y el valor es $(CONFIGURATION). El valor de CONFIGURACIÓN se establece automáticamente para nosotros. No tenemos que preocuparnos por ello. Como su nombre indica, el valor es igual al nombre de la configuración de construcción con la que se crea la construcción.

Update Info.plist

Revisa Configuration.swift. Queremos acceder fácilmente a la configuración del build, el valor almacenado en el archivo Info.plist del proyecto. Definimos una propiedad estática y constante, current, de tipo Configuration. Accedemos al valor almacenado en el fichero Info.plist para la clave Configuración y lo convertimos en una instancia String. Lanzamos un error fatal si esto falla porque eso nunca debería ocurrir.

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 el valor del archivo Info.plist para crear una instancia Configuration. Lanzamos otro error fatal si la inicialización falla. Observe que ponemos en minúsculas el valor almacenado en el archivo Info.plist. La instancia Configuration se devuelve desde el cierre. No olvides añadir un par de paréntesis al closure.

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 a probarlo. Abre AppDelegate.swift e imprime la configuración actual. Selecciona el esquema Staging, ejecuta la aplicación e inspecciona la salida en la consola.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.current) return true}
staging

Extendiendo la configuración

Probablemente sabes que me gusta aprovechar los enums para crear namespaces. Déjame mostrarte cómo podemos mejorar la implementación actual del enum Configuration. La solución más sencilla es definir una propiedad estática y computada, baseURL, de tipo URL. También se puede definir la propiedad como una propiedad estática y constante. Es una elección personal. Utilizamos una declaración switch para devolver la URL base para cada configuración de construcción.

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")! } }}

Hay varios detalles que me gustaría señalar. En primer lugar, no uso un caso default. Soy explícito sobre el valor que se devuelve para cada configuración de construcción. Esto facilita la detección de problemas y hace que el código sea más legible e intuitivo. En segundo lugar, utilizo el signo de exclamación para forzar a desenvolver el valor devuelto por el inicializador de la estructura URL. Este es uno de los raros escenarios en los que uso el signo de exclamación para forzar el desenvolvimiento de un valor. Es conveniente, pero, más importante, la URL base nunca debe ser igual a nil.

Abre AppDelegate.swift e imprime el valor de la propiedad computada baseURL. Con el esquema establecido en el Staging, ejecuta la aplicación e inspecciona la salida en la consola.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.baseURL) return true}
https://staging.cocoacasts.com

Este patrón tiene algunas ventajas sobre la primera solución que implementamos. Aprovechamos la seguridad de tipos de Swift y ya no estamos tratando con un valor opcional. También podemos beneficiarnos del autocompletado de Xcode. Estas ventajas son sutiles, pero se llegan a apreciar con el tiempo.

Pongamos la guinda al pastel añadiendo espacios de nombres a la mezcla. Crea un nuevo archivo Swift y nómbralo Configuration+DarkSky.swift.

Creating a Swift File

Creating a Swift File

Crea una extensión para el enum Configuration y define un enum con nombre DarkSky en la extensión. Dark Sky es un servicio meteorológico que uso de vez en cuando.

import Foundationextension Configuration { enum DarkSky { }}

El enum DarkSky define una propiedad estática y constante, apiKey, de tipo String. Conectamos la configuración actual y devolvemos un valor diferente para cada configuración de construcción. Como he mencionado antes, también puede declarar la propiedad como una propiedad estática, variable. Eso es hasta usted para 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" } }() }}

Hay varias ventajas a este enfoque. La configuración de la API de Cielo Oscuro es muy bien namespaced. Esto también hace que sea fácil de poner la configuración para la API de Dark Sky en un archivo separado.

Abrir AppDelegate.swift e imprimir la clave de la API para el servicio meteorológico de Dark Sky. Con el esquema establecido en Staging, ejecuta la aplicación e inspecciona la salida en la consola.

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.DarkSky.apiKey) return true}
123

¿Qué sigue?

Este patrón añade comodidad, seguridad de tipos y seguridad a tus proyectos. Es fácil de adoptar y, una vez implementado, es sencillo de extender. Es un patrón que me gusta utilizar por una serie de razones. Recomiendo encarecidamente que lo pruebes.