Managing Build Configurations in Xcode
The Missing Manual
for Swift Development
The Guide I Wish I Had When I Started Out
Join 20,000+ Developers Learning About Swift Development
Download Your Free Copy
Zupełnie nowy projekt Xcode definiuje dwie konfiguracje budowania, Debug i Release. Większość projektów z różnych powodów definiuje jedną lub więcej dodatkowych konfiguracji budowania. To nie jest nowość i dobrą praktyką jest używanie konfiguracji kompilacji, aby dostosować kompilację do specyficznych potrzeb środowiska, do którego ma zostać wdrożona.
W tym odcinku pokażę, jak bezpiecznie zarządzać danymi, które są specyficzne dla konfiguracji kompilacji, takimi jak klucze API, dane uwierzytelniające i inne wrażliwe dane. Istnieje kilka strategii zarządzania konfiguracjami budowania, ale istnieją istotne różnice, które należy rozważyć, zwłaszcza w kontekście bezpieczeństwa.
Dodawanie konfiguracji
Zapal Xcode i utwórz nowy projekt, wybierając szablon Single View App z sekcji iOS > Application.
Nadaj projektowi nazwę Configurations, ustaw Language na Swift i upewnij się, że pola wyboru na dole są odznaczone. Powiedz Xcode, gdzie chcesz przechowywać projekt i kliknij Utwórz.
Otwórz Nawigator projektu po lewej stronie i kliknij projekt u góry. Wybierz projekt w sekcji Projekt, aby wyświetlić szczegóły projektu. Zupełnie nowy projekt Xcode definiuje dwie konfiguracje kompilacji, Debug i Release.
Powszechnie używasz konfiguracji Debug podczas rozwoju, podczas gdy konfiguracja Release jest używana do tworzenia kompilacji App Store lub TestFlight. To prawdopodobnie nie jest dla Ciebie nowością.
Wiele aplikacji komunikuje się z backendem i powszechną praktyką jest posiadanie środowiska staging i production. Środowisko staging jest używane podczas tworzenia aplikacji. Kliknij dwukrotnie konfigurację Debug i zmień jej nazwę na Staging.
Dodajmy trzecią konfigurację dla środowiska produkcyjnego. Kliknij przycisk + na dole tabeli, wybierz Duplicate „Staging” Configuration i nazwij konfigurację Production.
Utwórzmy schemat dla każdej konfiguracji, aby ułatwić szybkie przełączanie między środowiskami. Zaznacz schemat na górze i wybierz z menu polecenie Zarządzaj schematami…. Wybierz schemat o nazwie Konfiguracje i kliknij na nim jeszcze raz. Zmień jego nazwę na Staging.
Z zaznaczonym schematem kliknij ikonę koła zębatego na dole i wybierz Duplikuj. Nadaj schematowi nazwę Produkcja. Wybierz Uruchom po lewej stronie i ustaw Konfigurację budowania na Produkcja.
To wszystko. Mamy teraz konfigurację budowania dla inscenizacji i produkcji. Dzięki schematom można szybko i łatwo przełączać się między konfiguracjami budowania.
User-Defined Build Settings
Jak wspomniałem wcześniej, istnieje kilka rozwiązań do zarządzania danymi, które są specyficzne dla danej konfiguracji budowania. W tym odcinku pokażę rozwiązanie, którego używam w każdym projekcie, który ma pewną złożoność. Zanim przedstawię rozwiązanie, które mam na myśli, chciałbym pokazać inne rozwiązanie, które jest często wykorzystywane przez programistów.
Wybierz projekt w Nawigatorze projektu po lewej stronie. Wybierz Configurations target z sekcji Targets i kliknij zakładkę Build Settings na górze.
Zakładka Build Settings pokazuje ustawienia budowania dla Configurations target. Możliwe jest rozszerzenie tej listy o ustawienia budowania, które sam określisz. Kliknij przycisk + na górze i wybierz Dodaj ustawienie zdefiniowane przez użytkownika.
Nazwij ustawienie zdefiniowane przez użytkownika BASE_URL. Definiowanie bazowego adresu URL jest powszechne w aplikacjach, które współdziałają z backendem.
Jaka jest korzyść z definiowania ustawienia zdefiniowanego przez użytkownika? Ustawienie zdefiniowane przez użytkownika pozwala nam ustawić wartość dla BASE_URL dla każdej konfiguracji budowania. Kliknij trójkąt po lewej stronie ustawienia zdefiniowanego przez użytkownika, aby wyświetlić listę konfiguracji budowania.
Ustaw wartość dla Production i Release na https://cocoacasts.com, a wartość dla Staging na https://staging.cocoacasts.com.
Jak sama nazwa wskazuje, ustawienie budowania jest dostępne podczas procesu budowania. Twój kod nie ma bezpośredniego dostępu do ustawień budowania, które definiujesz. Jest to powszechne błędne przekonanie. Jest jednak na to rozwiązanie. Otwórz Info.plist i dodaj nową parę klucz/wartość. Ustaw klucz na BASE_URL, a wartość na $(BASE_URL)
.
Jaka jest wartość dodania pary klucz-wartość do Info.plist projektu? Wartość pary klucz / wartość jest aktualizowana w czasie budowania. Xcode sprawdza ustawienia budowania dla bieżącej konfiguracji budowania i ustawia wartość klucza BASE_URL w Info.plist projektu.
Wypróbujmy to. Otwórz AppDelegate.swift i przejdź do metody application(_:didFinishLaunchingWithOptions:)
. Musimy uzyskać dostęp do zawartości pliku Info.plist. Jest to możliwe poprzez właściwość infoDictionary
klasy Bundle
. Interesujący nas bundle to main bundle. Jak sama nazwa wskazuje, właściwość infoDictionary
jest słownikiem par klucz-wartość, a my uzyskujemy dostęp do wartości dla klucza BASE_URL
.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Bundle.main.infoDictionary?) return true}
Wybierz schemat Production na górze i uruchom aplikację w symulatorze. Sprawdź wyjście w konsoli.
Optional(https://staging.cocoacasts.com)
Wybierz schemat Staging na górze i uruchom aplikację w symulatorze. Sprawdź dane wyjściowe w konsoli. Wygląda to całkiem nieźle. Right?
Optional(https://staging.cocoacasts.com)
A Word About Security
Większość programistów uważa to rozwiązanie za bardzo wygodne. I tak jest. Jest jednak jeden problem. Łatwo jest wyodrębnić plik Info.plist z aplikacji pobranych z App Store. Co się stanie, jeśli w pliku Info.plist przechowywane są klucze API, dane uwierzytelniające lub inne poufne informacje? Wprowadza to znaczne ryzyko bezpieczeństwa, którego możemy i powinniśmy unikać.
Chciałbym pokazać Ci proste rozwiązanie, które oferuje taką samą elastyczność, bezpieczeństwo typu i poprawione bezpieczeństwo. Utwórz nowy plik Swift i nazwij go Configuration.swift.
Zdefiniuj enum o nazwie Configuration i surowej wartości typu String
. Zdefiniujemy przypadek dla każdej konfiguracji budowania, staging
, production
i release
.
import Foundationenum Configuration: String { // MARK: - Configurations case staging case production case release}
Przed kontynuacją implementacji enum Configuration, musimy zaktualizować plik Info.plist. Otwórz plik Info.plist i dodaj parę klucz/wartość. Kluczem jest Configuration, a wartością $(CONFIGURATION)
. Wartość CONFIGURATION jest automatycznie ustawiana za nas. Nie musimy się o nią martwić. Jak sama nazwa wskazuje, wartość jest równa nazwie konfiguracji kompilacji, z którą tworzona jest kompilacja.
Odwiedź Configuration.swift. Chcemy mieć łatwy dostęp do konfiguracji budowania, wartości przechowywanej w pliku Info.plist projektu. Zdefiniuj statyczną, stałą właściwość, current
, typu Configuration
. Uzyskujemy dostęp do wartości, która jest przechowywana w pliku Info.plist dla klucza Configuration i rzutujemy ją na instancję String
. Wyrzucamy błąd, jeśli to się nie powiedzie, ponieważ nigdy nie powinno się to zdarzyć.
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") } }()}
Używamy wartości z pliku Info.plist, aby utworzyć instancję Configuration
. Jeśli inicjalizacja się nie powiedzie, wyrzucamy kolejny błąd krytyczny. Zauważ, że wartość zapisaną w pliku Info.plist piszemy małymi literami. Instancja Configuration
jest zwracana z zamknięcia. Nie zapomnij dodać pary nawiasów do zamknięcia.
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 }()}
Wypróbujmy to. Otwórz AppDelegate.swift i wydrukuj bieżącą konfigurację. Wybierz schemat Staging, uruchom aplikację i sprawdź dane wyjściowe w konsoli.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.current) return true}
staging
Extending Configuration
Prawdopodobnie wiesz, że lubię wykorzystywać enumy do tworzenia przestrzeni nazw. Pozwól, że pokażę Ci jak możemy poprawić obecną implementację enuma Configuration
. Najprostszym rozwiązaniem jest zdefiniowanie statycznej, wyliczanej właściwości baseURL
, typu URL
. Możesz również zdefiniować tę właściwość jako statyczną, stałą właściwość. Jest to wybór osobisty. Używamy instrukcji switch
, aby zwrócić bazowy adres URL dla każdej konfiguracji budowania.
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")! } }}
Jest kilka szczegółów, na które chciałbym zwrócić uwagę. Po pierwsze, nie używam przypadku default
. Jestem jednoznaczny co do wartości, która jest zwracana dla każdej konfiguracji budowania. Ułatwia to wykrycie problemów i sprawia, że kod jest bardziej czytelny i intuicyjny. Po drugie, używam wykrzyknika do wymuszenia rozpakowania wartości zwracanej przez inicjalizator struktury URL
. Jest to jeden z rzadkich scenariuszy, w których używam wykrzyknika do wymuszenia odwinięcia wartości. Jest to wygodne, ale, co ważniejsze, bazowy URL nigdy nie powinien być równy nil
.
Otwórz AppDelegate.swift i wypisz wartość baseURL
obliczonej właściwości. Ze schematem ustawionym na Staging, uruchom aplikację i zbadaj dane wyjściowe w konsoli.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.baseURL) return true}
https://staging.cocoacasts.com
Ten wzorzec ma kilka zalet w stosunku do pierwszego rozwiązania, które zaimplementowaliśmy. Korzystamy z bezpieczeństwa typu Swift i nie mamy już do czynienia z wartością opcjonalną. Możemy również skorzystać z autocompletion Xcode’a. Te zalety są subtelne, ale z czasem zaczynasz je doceniać.
Postawmy wisienkę na torcie, dodając do mieszanki przestrzenie nazw. Utwórz nowy plik Swift i nazwij go Configuration+DarkSky.swift.
Utwórz rozszerzenie dla enum Configuration i zdefiniuj enum o nazwie DarkSky
w rozszerzeniu. Dark Sky to serwis pogodowy, z którego korzystam od czasu do czasu.
import Foundationextension Configuration { enum DarkSky { }}
Enum DarkSky
definiuje statyczną, stałą właściwość, apiKey
, typu String
. Włączamy bieżącą konfigurację i zwracamy inną wartość dla każdej konfiguracji budowania. Jak wspomniałem wcześniej, możesz również zadeklarować tę właściwość jako statyczną, zmienną właściwość. Decyzja należy do Ciebie.
import Foundationextension Configuration { enum DarkSky { static let apiKey: String = { switch Configuration.current { case .staging: return "123" case .production: return "456" case .release: return "789" } }() }}
Jest kilka zalet tego podejścia. Konfiguracja dla Dark Sky API jest ładnie oznaczona nazwami. Ułatwia to również umieszczenie konfiguracji dla interfejsu API Dark Sky w osobnym pliku.
Otwórz AppDelegate.swift i wypisz klucz API dla usługi pogodowej Dark Sky. Ze schematem ustawionym na Staging, uruchom aplikację i sprawdź dane wyjściowe w konsoli.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: ?) -> Bool { print(Configuration.DarkSky.apiKey) return true}
123
What’s Next?
Ten wzorzec dodaje wygodę, bezpieczeństwo typu i bezpieczeństwo do Twoich projektów. Jest łatwy do zaadoptowania, a gdy już zostanie zaimplementowany, jest prosty do rozszerzenia. Jest to wzorzec, który lubię używać z wielu powodów. Gorąco polecam, aby go wypróbować.