Hantera byggkonfigurationer i Xcode
The Missing Manual
for Swift Development
Guiden jag önskar att jag hade haft när jag började
Tillhör 20,000+ utvecklare som lär sig om Swift-utveckling
Ladda ner ditt gratisexemplar
Ett helt nytt Xcode-projekt definierar två byggkonfigurationer, Debug och Release. De flesta projekt definierar en eller flera ytterligare byggkonfigurationer av olika skäl. Detta är inte nytt och det är en bra metod att använda byggkonfigurationer för att skräddarsy en build till de specifika behoven i den miljö som den ska distribueras till.
I det här avsnittet visar jag dig hur du på ett säkert sätt hanterar data som är specifika för en byggkonfiguration, till exempel API-nycklar, autentiseringsuppgifter och andra känsliga data. Det finns flera strategier för att hantera byggkonfigurationer, men det finns viktiga skillnader som du måste ta hänsyn till, särskilt i säkerhetssammanhang.
Lägg till en konfiguration
Förstör Xcode och skapa ett nytt projekt genom att välja mallen Single View App i avsnittet iOS > Application.
Namnera projektet Configurations, ställ in Språk till Swift och se till att kryssrutorna längst ner är avmarkerade. Berätta för Xcode var du vill lagra projektet och klicka på Skapa.
Öppna projektnavigatorn till vänster och klicka på projektet högst upp. Välj projektet i avsnittet Projekt för att visa projektdetaljerna. Ett helt nytt Xcode-projekt definierar två byggkonfigurationer, Debug och Release.
Du använder vanligen Debug-konfigurationen under utveckling medan Release-konfigurationen används för att skapa App Store- eller TestFlight-byggen. Detta är förmodligen inte nytt för dig.
Många program kommunicerar med en backend och det är vanligt att ha en staging- och en produktionsmiljö. Stagingmiljön används under utvecklingen. Dubbelklicka på konfigurationen Debug och döpa om den till Staging.
Låt oss lägga till en tredje konfiguration för produktionsmiljön. Klicka på +-knappen längst ner i tabellen, välj Duplicera ”Staging”-konfiguration och namnge konfigurationen Production.
Låt oss skapa ett schema för varje konfiguration för att göra det enkelt att snabbt växla mellan miljöer. Markera schemat högst upp och välj Manage Schemes… från menyn. Välj schemat som heter Configurations och klicka på det en gång till. Byt namn till Staging.
Med schemat markerat klickar du på kugghjulsikonen längst ner och väljer Duplicera. Ge schemat namnet Production. Välj Kör till vänster och ställ in Byggkonfiguration till Produktion.
Det var allt. Vi har nu en byggkonfiguration för staging och produktion. Schemana gör det snabbt och enkelt att växla mellan byggkonfigurationer.
Användardefinierade bygginställningar
Som jag nämnde tidigare finns det flera lösningar för att hantera data som är specifika för en viss byggkonfiguration. I det här avsnittet visar jag en lösning som jag använder i alla projekt som har en viss komplexitet. Innan jag lägger ut den lösning jag har i åtanke vill jag visa dig en annan lösning som ofta används av utvecklare.
Välj projektet i projektnavigatorn till vänster. Välj målet Configurations i avsnittet Targets och klicka på fliken Build Settings högst upp.
Fliken Build Settings visar bygginställningarna för målet Configurations. Det är möjligt att utöka den här listan med bygginställningar som du definierar. Klicka på +-knappen högst upp och välj Lägg till användardefinierad inställning.
Nämn den användardefinierade inställningen BASE_URL. Att definiera en bas-URL är vanligt för program som interagerar med en backend.
Vad är fördelen med att definiera en användardefinierad inställning? Med den användardefinierade inställningen kan vi ange ett värde för BASE_URL för varje byggkonfiguration. Klicka på triangeln till vänster om den användardefinierade inställningen för att visa listan över byggkonfigurationer.
Sätt värdet för Production and Release till https://cocoacasts.com och värdet för Staging till https://staging.cocoacasts.com.
Som namnet antyder är en bygginställning tillgänglig under byggprocessen. Din kod kan inte få direkt tillgång till de bygginställningar som du definierar. Detta är en vanlig missuppfattning. Det finns dock en lösning. Öppna Info.plist och lägg till ett nytt nyckel/värdepar. Ställ in nyckeln till BASE_URL och värdet till $(BASE_URL)
.
Vad är värdet av att lägga till ett nyckel/värdepar i projektets Info.plist? Värdet för nyckel/värdeparet uppdateras vid byggnadstillfället. Xcode inspekterar bygginställningarna för den aktuella byggkonfigurationen och fastställer värdet för nyckeln BASE_URL i projektets Info.plist.
Vi provar det. Öppna AppDelegate.swift och navigera till metoden application(_:didFinishLaunchingWithOptions:)
. Vi måste få tillgång till innehållet i filen Info.plist. Detta är möjligt genom egenskapen infoDictionary
i klassen Bundle
. Det paket som vi är intresserade av är huvudpaketet. Som namnet antyder är egenskapen infoDictionary
en ordbok med nyckel/värdepar och vi får tillgång till värdet för nyckeln BASE_URL
.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Bundle.main.infoDictionary?) return true}
Välj Produktionsschema högst upp och kör programmet i simulatorn. Inspektera utdata i konsolen.
Optional(https://staging.cocoacasts.com)
Välj Staging-schemat högst upp och kör programmet i simulatorn. Kontrollera resultatet i konsolen. Det är ganska trevligt. Eller hur?
Optional(https://staging.cocoacasts.com)
Ett ord om säkerhet
De flesta utvecklare tycker att den här lösningen är mycket praktisk. Och det är det också. Det finns dock ett problem. Det är lätt att extrahera filen Info.plist från program som laddats ner från App Store. Vad händer om du lagrar API-nycklar, autentiseringsuppgifter eller annan känslig information i filen Info.plist? Detta medför en betydande säkerhetsrisk som vi kan och bör undvika.
Jag vill visa dig en enkel lösning som erbjuder samma flexibilitet, typsäkerhet och förbättrad säkerhet. Skapa en ny Swift-fil och namnge den Configuration.swift.
Definiera ett enum med namnet Configuration och ett råvärde av typen String
. Vi definierar ett fall för varje byggkonfiguration, staging
, production
och release
.
import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release}
För att fortsätta med implementeringen av Configuration enum måste vi uppdatera filen Info.plist. Öppna Info.plist och lägg till ett nyckel/värdepar. Nyckeln är Configuration och värdet är $(CONFIGURATION)
. Värdet CONFIGURATION är automatiskt inställt för oss. Vi behöver inte oroa oss för det. Som namnet antyder är värdet lika med namnet på den byggkonfiguration som byggnaden skapas med.
Visa Configuration.swift. Vi vill ha enkel åtkomst till byggkonfigurationen, det värde som lagras i projektets fil Info.plist. Definiera en statisk, konstant egenskap, current
, av typen Configuration
. Vi får tillgång till det värde som lagras i filen Info.plist för nyckeln Configuration och kastar det till en String
-instans. Vi kastar ett fatalt fel om detta misslyckas eftersom det aldrig får hända.
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") } }()}
Vi använder värdet från filen Info.plist för att skapa en Configuration
-instans. Vi kastar ett annat dödligt fel om initialiseringen misslyckas. Lägg märke till att vi skriver värdet som lagras i filen Info.plist med små bokstäver. Instansen Configuration
returneras från stängningen. Glöm inte att lägga till ett par parenteser till stängningen.
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 }()}
Låt oss prova det. Öppna AppDelegate.swift och skriv ut den aktuella konfigurationen. Välj Staging-schemat, kör programmet och inspektera utdata i konsolen.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.current) return true}
staging
Extending Configuration
Du vet säkert att jag gillar att utnyttja enums för att skapa namnområden. Låt mig visa dig hur vi kan förbättra den nuvarande implementeringen av Configuration
enum. Den enklaste lösningen är att definiera en statisk, beräknad egenskap, baseURL
, av typen URL
. Du kan också definiera egenskapen som en statisk, konstant egenskap. Det är ett personligt val. Vi använder ett switch
-meddelande för att returnera bas-URL:n för varje byggkonfiguration.
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")! } }}
Det finns flera detaljer som jag vill påpeka. För det första använder jag inte ett default
-fall. Jag är tydlig med vilket värde som returneras för varje byggkonfiguration. Detta gör det lättare att upptäcka problem och det gör koden mer läsbar och intuitiv. För det andra använder jag utropstecknet för att tvinga fram en avveckling av det värde som returneras av initialisatorn för strukturen URL
. Detta är ett av de sällsynta scenarier där jag använder utropstecken för att tvinga fram ett värde. Det är praktiskt, men viktigare är att bas-URL:en aldrig får vara lika med nil
.
Öppna AppDelegate.swift och skriv ut värdet på den baseURL
beräknade egenskapen. Med schemat inställt på Staging kör du programmet och inspekterar resultatet i konsolen.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.baseURL) return true}
https://staging.cocoacasts.com
Det här mönstret har några fördelar jämfört med den första lösningen vi implementerade. Vi drar nytta av Swifts typsäkerhet och vi har inte längre att göra med ett valfritt värde. Vi kan också dra nytta av Xcodes autokomplettering. Dessa fördelar är subtila, men man börjar uppskatta dem med tiden.
Låt oss sätta körsbäret på kakan genom att lägga till namespaces i mixen. Skapa en ny Swift-fil och namnge den Configuration+DarkSky.swift.
Skapa ett tillägg för Configuration enum och definiera ett enum med namnet DarkSky
i tillägget. Dark Sky är en vädertjänst som jag använder då och då.
import Foundationextension Configuration { enum DarkSky { }}
Den DarkSky
enum definierar en statisk, konstant egenskap, apiKey
, av typen String
. Vi slår på den aktuella konfigurationen och returnerar ett annat värde för varje byggkonfiguration. Som jag nämnde tidigare kan du också deklarera egenskapen som en statisk, variabel egenskap. Det är upp till dig att bestämma.
import Foundationextension Configuration { enum DarkSky { static let apiKey: String = { switch Configuration.current { case .staging: return "123" case .production: return "456" case .release: return "789" } }() }}
Det finns flera fördelar med det här tillvägagångssättet. Konfigurationen för Dark Sky API är snyggt namngiven. Detta gör det också enkelt att lägga konfigurationen för Dark Sky API i en separat fil.
Öppna AppDelegate.swift och skriv ut API-nyckeln för väderlekstjänsten Dark Sky. Med schemat inställt på Staging kör du programmet och inspekterar utmatningen i konsolen.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.DarkSky.apiKey) return true}
123
Vad händer härnäst?
Det här mönstret tillför bekvämlighet, typsäkerhet och säkerhet till dina projekt. Det är lätt att införa och när det väl är implementerat är det okomplicerat att utöka det. Det är ett mönster som jag gärna använder av en rad olika skäl. Jag rekommenderar starkt att du ger det ett försök.