Using Swift Modules

In this blog post, I refactor an existing repository by incorporating Swift modules. While certain aspects of this process were smooth sailing, I encountered challenges along the way. Join me as I share my experiences and insights.

The source code is hosted on GitHub in this repository: https://github.com/field-theory-org/match-game-architecture.git.

The Project Background

My post series on app architecture starts in this post. Throughout the series, I showcase an app implemented using different architectural patterns, emphasizing the advantages and disadvantages of each approach. The distinct variants of the app share code, particularly reusing the data model and view. Since the project’s inception in 2016, the Swift programming language and its toolchain have made significant strides. In this installment, I demonstrate how to enhance the project by transitioning from linking source code files to employing Swift modules.

The Original Approach

The basic structure of the repository is as follows:

├── 01_Common
│   ├── CustomViews
│   ├── Model
│   └── Resources
├── 10_Commandline-App
├── 20_Basic-App
├──...

Initially, I had just used symbolic links from a common source file in the 01_Common folder to the specific usage in each of the projects. This approach was problematic because Xcode was not good at understanding how to handle symbolic links – once you edit a file, it’ll get confused.

Moreover, the shared code lacked organization within a dedicated project, rendering it impossible to build and test in isolation. Ultimately, this approach was not only suboptimal but also inelegant.

The Solution

The traditional approach to addressing shared code issues is to put the shared code into a framework. With its own project, a framework can include tests. While this remains a viable solution today, Swift modules present an alternative. Here, I outline the advantages and disadvantages of both options:

  • Both Swift modules and frameworks enable code modularization, and thus share and reuse the code across different projects. For the current code-base either choice would be suitable.
  • Both can be distributed independently as separate units.
  • Both support versioning.
  • A framework is pre-compiled, and as such, cannot be reduced in size. Consequently, they are fully included in a project, particularly increasing app load times.
  • Swift modules are more lightweight compared to frameworks, with less overhead related to dynamic linking. Ultimately, a large Swift module loads faster than a similarly sized framework.
  • Swift Package Manager has its own dependency management out of the box. For frameworks, you need third-party solutions like CocoaPods.
  • Swift modules can also be used in other projects, such as Swift on Linux, whereas frameworks are restricted to the Apple platform.
  • Frameworks can be used both in Swift and Objective-C code bases. Swift modules only work with the former.

Additionally, I encountered another limitation:

  • Frameworks have their own dedicated Xcode project. Thus, they support tests for each target platform. However, I discovered that testing code reliant on UIKit in Swift modules is not feasible without creating a separate, dedicated Xcode project.

Such a project is usually not needed when running platform-independent tests.

In the specific case of the match game app, there are two separate groups of functionality. Thus, I have divided the functionality into two separate modules:

  1. The data model, and
  2. the custom view.

I have reorganized the code in 01_Common accordingly:

├── 01_Common
│   ├── MatchModel
│   │   ├──Sources
│   │   ├──Tests
│   ├── MatchView
│   │   ├──Sources
│   └── Resources
├── 10_Commandline-App
├── 20_Basic-App
├──...
  1. The data model is in the MatchModel module.
  2. The view is in the MatchView module.
  3. The resources remain untouched.

The data model has a suite of tests that can be run on the macOS command line directly using

1
2
cd 01_Common/MatchModel
swift test

Note that this is not possible to run any actions on the command line with swift for the MatchView module. The view can only be built and tested using xcodebuild, which necessitates a separate Xcode project file.

The first project in 10_Commandline-App now requires only the MatchModel module, whereas the second project in 20_Basic-App needs both the MatchModel and MatchView modules. The third project in 30_MVVM-App must be adapted in the same way as the basic iOS app.

In addition, I have carried out the following steps:

  • Introduced workspaces for all projects that use modules, which entails:
    • Utilizing the workspace instead of the project file for all command line and CI build actions.
    • Opening the workspace in Xcode rather than the project file.
  • Explicitly included both modules in the list of library dependencies in the target build settings.
  • Fixed the dependency of the MatchPileView in the iOS storyboards, as it is now part of the external module rather than the project itself.
  • Added import MatchModel and import MatchView explicitly in the source code, since they now belong to modules instead of the main source code base.

Ultimately, the project compiles and runs smoothly, utilizing modern tools and workflows.

Stay tuned for the next installment in this series!