🧱Project Foundation

Spring 2024 | Vin Bui

This chapter focuses on the important fundamentals that we must think about when creating a brand new project. As a disclaimer, there are various ways to create a project from scratch, but this is just how I prefer to do things.

Architecture Patterns

The first thing that we must think about is the structure of our codebase. This often depends on the framework that we are using. For example, it’s more common to use MVC (Model-View-Controller) in UIKit and MVVM (Model-View-ViewModel) in SwiftUI. There are also other popular architectures such as TCA (The Composable Architecture) and VIPER.

How do we know which one to use? The answer is, it depends. Some architectures are more friendly when working with a large team such as VIPER which emphasizes more files with less code. It’s important that we choose the architecture that best suits our needs and our environment. Despite which pattern we choose, it’s more important that we stick with it and build on top of it.

Project Directory Structure

Now that we have chosen an architecture pattern, we can now create folders and other subdirectories to structure our project. This is the structure that I like to use with MVVM and highly recommend:

  • Configs: the app environment settings, like production, staging, and development configurations, all using the .xcconfig files.

  • Core: the app’s entry point.

  • Models: model objects used by the API and throughout the app.

  • Resources: the project assets, LaunchScreen.storyboard, etc.

  • Services: service helpers such as a Networking API service, CoreData, SwiftData, UserDefaults, etc.

    • Networking: our app’s Networking API service. No need to create this folder if there is only one file.

  • Utils: other helper files such as constants, extensions, custom errors, etc.

  • ViewModels: our app’s view models which implement properties and commands to which the view can data bind to and notify the view of any state changes. If using MVC, replace this with Controllers.

    • Inside of this folder, we can create a folder for each “main” section of our app.

  • Views: the appearance and UI of the app.

    • Inside of this folder, we can create a folder for each “main” section of our app.

    • We can also create a Supporting folder which contains reusable views that are used throughout the app.

Dependency Manager

Next up, we must think about how to manage dependencies. Most of the time, we will be importing code written by others so it’s important that we decide which dependency manager to use. There are various managers we can use such as Swift Package Manager, CocoaPods, Carthage, etc.

I recommend using Swift Package Manager (SPM) since it is the newest of them all and is highly supported by Apple. It works seamlessly with Xcode and is very simple to use. If there are packages not available through Swift Package Manager, the next best alternative is CocoaPods. CocoaPods was a very popular choice before SPM was introduced, and it works with other operating systems such as MacOS as well! Note that we can mix SPM with other managers. Learn more about CocoaPods below.

Debug and Release

In many applications, we often have multiple environments: a staging environment (also known as development) and a production environment. As a result, our iOS code may need to change depending on the environment. For example, if we are logging Analytics, we only want to log during a production environment.

To handle this logic, we typically use a #if DEBUG or #if RELEASE statement. How does this work? If we click on our scheme at the top of Xcode and look at Build Configuration, we can see that there are two options: Debug and Release.

The most common way to change between environments is to create two different schemes: one that uses a Debug configuration, and one that uses a Release configuration. This way, we can easily change the environment by simply changing the scheme.

Secrets and Environment Variables

Now that we have introduced debug and release configurations, let’s talk about secrets and environment variables. These are essentially values that are set outside of our program but are crucial in running our application. Additionally, these are commonly keys or files that contain sensitive information and should not be pushed into a public repository (should be on a .gitignore).

Let’s say we have a backend endpoint that we want to (and should) keep hidden from the public. We can include the value in a .plist or a .xcconfig file.

XCConfig

If using the xcconfig approach, we can create a file called Keys.xcconfig to store our environment variables. Note the usage of capital letters and underscores.

DEV_URL = https:/$()/mybackendserver-dev.com
PROD_URL = https:/$()/mybackendserver-prod.com

To use these variables, we must import them through our app’s Info.plist. In the example above, we can create a new key called DEV_URL with the type String and value $(DEV_URL). A dollar sign with parentheses is required and must match exactly with the value in the xcconfig file.

Finally, in Project > Info > Configurations, set the configuration to use our xcconfig file.

Accessing values from a property list

To access values from a property list (plist), let’s first create an enum to represent our app’s environment. We will use Uplift in this example.

/// An enumeration representing Uplift's environment variables.
enum UpliftEnvironment {
    /// Keys from Info.plist.
    enum Keys {
#if DEBUG
        static let baseURL: String = "DEV_URL"
#else
        static let baseURL: String = "PROD_URL"
#endif
    }

    /// A dictionary storing key-value pairs from Info.plist.
    private static let infoDict: [String: Any] = {
        guard let dict = Bundle.main.infoDictionary else {
            fatalError("Info.plist not found")
        }
        return dict
    }()

    /**
     The base URL of Uplift's backend server.

     * If the scheme is set to DEBUG, the development server URL is used.
     * If the scheme is set to RELEASE, the production server URL is used.
     */
    static let baseURL: URL = {
        guard let baseURLString = UpliftEnvironment.infoDict[Keys.baseURL] as? String,
              let baseURL = URL(string: baseURLString) else {
            fatalError("Base URL not found in Info.plist")
        }
        return baseURL
    }()
}

The static computed property infoDict converts our Info.plist file to a dictionary. We can then access this value just like any dictionary in Swift (as seen in the baseURL computed property). Notice the usage of #if DEBUG to determine the value of Keys.baseURL.

If we used a custom property list instead of a config file, then Bundle.main.infoDictionary will not work. Use the following code instead:

if let path = Bundle.main.path(forResource: "<NAME_OF_FILE>", ofType: "plist"),
   let dict = NSDictionary(contentsOfFile: path) as? [String: AnyObject] {
    // Use dictonary here...
}

Constants File

Most of the time, our application’s user interface will be created by some designer. The design will typically follow some kind of design system which includes the app’s font choices, colors, images, etc. To have full control of our application, we typically create a constants file which contains constants used throughout our app.

Now, there are many ways to represent these constants. One approach would be to use extensions. For example, we can create the following Color extension:

extension Color {
    /// Singleton instance
    static let volume = Volume()

    /// A structure containing colors used in Volume's design system.
    struct Volume {
        let backgroundGray = Color(white: 245 / 255)
        let buttonGray = Color(white: 238 / 255)
        let buttonDisabledGray = Color(red: 244/255, green: 239/255, blue: 239/255)
        let errorRed = Color(red: 203/255, green: 46/255, blue: 46/255)
    }
}

This is a pretty good approach because we can simply use Color.volume.backgroundGray to access the color. However, this is not the ideal usage of extensions and requires multiple files to be created.

We can do better by creating a Constants.swift file inside of our Utils folder.

/// A structure containing constants used in Volume's design system.
struct Constants {
    /// Colors used in Volume's design system.
    enum Colors {
        // Primary
        static let backgroundGray = Color(white: 245 / 255)
        static let buttonGray = Color(white: 238 / 255)
        static let buttonDisabledGray = Color(red: 244/255, green: 239/255, blue: 239/255)
        static let errorRed = Color(red: 203/255, green: 46/255, blue: 46/255)
    }

    enum Fonts {
        // H Headers
        static let h0 = Font.custom("Montserrat-Bold", size: 36)
        static let h1 = Font.custom("Montserrat-Bold", size: 24)
        static let h2 = Font.custom("Montserrat-Bold", size: 16)
        static let h3 = Font.custom("Montserrat-Bold", size: 14)
    }
}

We create a struct called Constants which contains multiple caseless enums. Using caseless enums is preferred over structs because we do not have to worry about any object-oriented logic such as instantiation, methods, properties, etc. To use a color, we can simply do Constants.Colors.backgroundGray. Now, our app’s constants are all located in a single file, making it much easier to change if needed.

Linter + Logging

The final important fundamentals we need to think about are linting and logging, both of which involve making our code readable and easy to digest by other developers.

Linting

The most popular linting library for Swift is SwiftLint, which can be installed with almost any dependency manager. We won’t go over how to use SwiftLint since their documentation explains it pretty well, but with SwiftLint, we can define rules and even make exceptions for some lines.

Logging

It’s also very important that we have a system of logging our applications’ error messages, debug messages, warnings, etc. The recommended way to do this is by using OSLog which works seamlessly with the Xcode 15 console. Learn more about it here.

Other Topics

Below are a few other topics that we may also need to consider when creating our app’s foundation.

  • Image Caching (Nuke, KingFisher, SDWebImage, etc.)

  • Networking

  • Unit/UI Tests

  • CI/CD (fastlane, Jenkins, Xcode Cloud)

  • Analytics + Crashlytics

Last updated