In the previous episode, we hard-coded the base URL and the API key of the Clear Sky API in the WeatherClient class. I'm not a fan of hard-coding configuration details. In this episode, we explore an alternative approach.

Defining User-Defined Build Settings

The plan is to define the base URL and API key of the Clear Sky API as user-defined build settings. Let me show you how that works. Select the project from the Project Navigator on the left, select the Thunderstorm target, and open the Build Settings tab at the top. Scroll to the User-Defined section at the bottom.

Click the + button at the top to add a user-defined build setting for the base URL of the Clear Sky API. The name of the build setting is CLEAR_SKY_BASE_URL and its value is the base URL of the Clear Sky API. We repeat these steps for the API key of the Clear Sky API.

Defining User-Defined Build Settings

The benefit of a build setting is that you can define a different value for each build configuration. For example, we could set the base URL of the Clear Sky API to a URL that points to a staging or development server for the Debug build configuration. That is a common use case.

Defining User-Defined Build Settings

Storing User-Defined Build Settings in Info.plist

As the name suggests, a build setting is used to define or configure a build. The problem is that a build setting isn't accessible at runtime. We can work around this limitation by storing the build setting in the target's Info.plist.

Open the Info tab at the top, right-click one of the keys in the Custom iOS Target Properties section, and choose Add Row from the contextual menu. Set Key to CLEAR_SKY_BASE_URL. The value is set at build time. The compiler reads the user-defined build setting and writes the value to the target's Info.plist. We need to stick to a convention to make that happen. The value is a dollar sign followed by the name of the user-defined build setting wrapped in parentheses.

Storing User-Defined Build Settings in Info.plist

We repeat these steps for the API key of the Clear Sky API. Notice that Xcode adds a file with name Info.plist to the project the moment we added a row to the Custom iOS Target Properties section. The target's Info.plist is missing by default. Xcode adds the target's Info.plist the moment we modify the target's properties.

Reading Values from the Target's Info.plist

The next step is creating a convenient API to read the values we defined in the target's Info.plist. Add a Swift file to the Utilities group and name it Configuration.swift. Declare an enum with name Configuration.

import Foundation

enum Configuration {
	
}

We can access the values we defined in the target's Info.plist through the Bundle class. Declare a private, static, variable property with name infoDictionary. The infoDictionary property is a dictionary with keys of type String and values of type Any. In the body of the computed property, we obtain a reference to the main bundle singleton through the main class property of the Bundle class. The main bundle singleton provides access to the key-value pairs defined in the target's Info.plist through its infoDictionary property. Because the infoDictionary property is of an optional type, we fall back to an empty dictionary.

import Foundation

enum Configuration {

    // MARK: - Properties

    private static var infoDictionary: [String: Any] {
        Bundle.main.infoDictionary ?? [:]
    }

}

I want to avoid string literals so we declare a private enum with name Keys. The Keys enum defines two static, constant properties, clearSkyBaseURL and clearSkyAPIKey. The values of these properties correspond with the keys we defined earlier in the target's Info.plist.

import Foundation

enum Configuration {

    // MARK: - Types

    private enum Keys {
        static let clearSkyBaseURL = "CLEAR_SKY_BASE_URL"
        static let clearSkyAPIKey = "CLEAR_SKY_API_KEY"
    }

    // MARK: - Properties

    private static var infoDictionary: [String: Any] {
        Bundle.main.infoDictionary ?? [:]
    }

}

We define a static, computed property for each key-value pair we defined in the target's Info.plist. I find that most convenient. Declare a static, computed property with name clearSkyBaseURL of type URL. In the body of the computed property, we access the value through the infoDictionary property we defined earlier and use the value to create and return a URL object.

static var clearSkyBaseURL: URL {
    let urlAsString = infoDictionary[Keys.clearSkyBaseURL] as! String
    return URL(string: urlAsString)!
}

Notice that we force cast the value to a string and force unwrap the result of the initializer of the URL struct. These operations should never fail. If they do, we made a mistake we need to fix.

Declare another static, computed property with name clearSkyAPIKey of type String. In the body of the computed property, we access the value through the infoDictionary property we defined earlier. We force cast the value to a string because that operation should never fail.

static var clearSkyAPIKey: String {
    infoDictionary[Keys.clearSkyAPIKey] as! String
}

Using the Values

Let's update the implementation of the WeatherClient class by removing the hard-coded configuration details. Open WeatherClient.swift and declare a private, constant property with name apiKey of type String. We also remove the value of the baseURL property.

import Foundation

final class WeatherClient: WeatherService {

    // MARK: - Properties

    private let baseURL: URL
    private let apiKey: String

	...

}

Replace the hard-coded API key in the weather(for:) method with the value stored in the apiKey property.

// MARK: - Methods

func weather(for location: Location) async throws -> WeatherData {
    let queryItems: [URLQueryItem] = [
        .init(name: "lat", value: "\(location.latitude)"),
        .init(name: "long", value: "\(location.longitude)"),
        .init(name: "api_key", value: apiKey)
    ]

    ...
}

We need to define an initializer that accepts the base URL and the API key. Because we expect the values for the baseURL and apiKey parameters to always come from the Configuration enum, we set those as the default values of the baseURL and apiKey parameters. This is optional, though. If you prefer to explicitly pass the base URL and API key to the initializer, then that is fine too.

// MARK: - Initialization

init(
    baseURL: URL = Configuration.clearSkyBaseURL,
    apiKey: String = Configuration.clearSkyAPIKey
) {
    self.baseURL = baseURL
    self.apiKey = apiKey
}

What's Next?

It is important to note that it isn't difficult for a third party to access the key-value pairs of the application's Info.plist. Be mindful of the type of data you store in the application's Info.plist. While hard-coding sensitive information is something you need to avoid if possible, note that the application's Info.plist shouldn't be used either if the data is sensitive, for example, a private key. I talk more about this in Protecting the Secrets of Your Mobile Application.