Xcode project setup for (many) Swift modules

Christian König
grandcentrix

--

Introduction

This is a summary of our experience with Swift modules for app features, reusable components and subsystems for a single iOS project. We don’t share our modules with other projects.

Conclusion

The key factor for making it easy to create new modules is leveraging .xcconfig files for managing build settings.

Goals

  • Make it as easy as possible to create new modules
  • Put new features and components in modules
  • Move existing features, components and subsystems from app target to modules
  • Replace all Objective-C code with Swift code

Motivation

  • Reduce conflict potential in Xcode project file
  • Force sepereration of concerns and small APIs
  • Better test coverage
  • Faster build and testing times
  • Option to replace individual modules with binary versions

Starting point

Since the project started in 2013, the codebase has grown considerably to 67706 lines of Objective-C code (84%) and 13210 lines of Swift code (16%) in 2018. At that point we realized that having all the code in the app target and having only one unit test target might be slowing it down.

We decided to lay down the groundwork for creating modules using framework targets, so that we are in the position to develop new features as modules.

Today

In 2019, the project has 3 framework targets containing features and components. Each framework target has its own unit test target which is run with a special test host app. Compared to running the test target with the main app, this has the advantage that startup is a lot quicker and it’s ensured that unit tests can’t access symbols from the app or its many dependencies.

The codebase still contains a mixture of Objective-C (65%) and Swift (35%), but modules are pure Swift. We are very happy with our approach and we will be moving more components and features into modules during the next years.

Creating new modules

In the project root we have a set of .xcconfig files, one for framework targets and one for test targets. They contain the complete configuration that is required for new targets and a developer does not have to change a single build setting after creating new targets.

Steps

This makes the process of creating new modules or test targets very simple:

  1. Create new framework and/or test target
  2. Delete all the files that have been created automatically
  3. Go to Configruations in the Info section of the project settings and set framework.xcconfig or test.xcconfig as the configuration file for your new target
  4. Go to Build Settings of your new target(s) and delete every setting that is defined by the target configuration (every required setting should come from the .xcconfig file)
  5. Create folders at the location of your choice
  6. Start adding files to the new targets (making sure they are assigned to the correct target)
  7. Verify that the Test action of your framework’s scheme is configured to run the test target’s unit tests

Process review

  • Developer does not have to change build settings to add new module, only delete them
  • Manually deleting all the build settings defined by a newly created target is not ideal
  • Maybe there is a way to add targets without default settings?
  • Swift package manager could eventually solve this problem at some point in the future
  • Not having modules in the project root is nice
  • Having a consistent folder structure for modules is nice
  • Needing only to build the module you are working on is nice

Hints and suggestions

  • .xcconfig files can include other .xcconfig files, so you can create a set of shared build settings
  • You can have different .xcconfig files for each of your configurations (Debug, Release, AppStore etc.), but you can also have just a single .xcconfig file to manage configuration-dependent settings for all configurations
  • Make sure that all of the relevant build settings of your module targets are defined by the .xcconfig files
  • Remember the goal is that developers don’t need to configure anything in the build settings editor when they create a new target
  • You can use a single Info.plist file for all the framework targets and test targets
  • We generate the bundle identifier from the target name
  • Use a subfolder for the modules (e.g. modules/)
  • Use a template folder structure for each module (e.g. sources/, resources/, tests/)
  • Put a README.md in each module
  • Generate .xcconfig files for existing projects with BuildSettingExtractor
  • The Quick Help Inspector can show a summary for each build setting

.xcconfig file example

This is a part of our configuration for framework targets. If you are not sure what settings you need, you can use Xcode's Build Settings Editor to setup and test everything and then generate the .xcconfig files with BuildSettingExtractor.

DEVELOPMENT_TEAM[config=Debug] = ABC
DEVELOPMENT_TEAM[config=Release] = XYZ
DEVELOPMENT_TEAM[config=AppStore] = XYZ
CODE_SIGN_STYLE = Manual
DEFINES_MODULE = YES
INFOPLIST_FILE = Modules/Info.default.plist
LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks
PRODUCT_BUNDLE_IDENTIFIER = net.grandcentrix.$(TARGET_NAME)
PRODUCT_NAME = $(TARGET_NAME)
PROVISIONING_PROFILE_SPECIFIER =
TARGETED_DEVICE_FAMILY = 1

FAQ

Q: Should I use modules even if I don’t plan to share them between projects?
A: Definitely. (see Motivation)

Q: How do I know what to put into a module?
A: There is no easy answer for that. A module shouldn’t be too small and shouldn’t be too big. A good idea might be to group by functionality. Having a lot of dependencies is an indication that the module has too many responsibilities. Examples: Reusable UI components; a feature with UI, models and business logic; services and models for talking to an API.

Q: Is this going to slow down development?
A: No. (Since everything is still in the same project and repository we don’t have to deal with dependency management for our own modules)

Q: Are there any drawbacks?
A: So far we have not encountered any.

Q: Can you replace modules with versioned binaries?
A: Yes. (At some point a module might not be changed frequently any more, that is a good time to think about replacing the code with a pre-built binary)

--

--