🧱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.
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.
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:
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:
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.
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