How to modularize existing iOS projects using Swift Package

⋅ 8 min read ⋅ Development Swift Package Module iOS

Table of Contents

Modular programming is a software design technique that breaks your project into a smaller maintainable module which promotes separation of concern and reusability.

Today, I will show you how to modularize an iOS app using Swift Package. Modularizing your app may sound technical and complicated, but in reality, it is just as easy as grouping your files in a folder and you don't need to modularize everything at the very beginning. You can incrementally adopt this part by part at your own pace.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Overview

There are only five small steps we need to do to modularize an app.

  1. Create a new Swift Package within your project.
  2. Move files from the main project to the newly created Swift Package.
  3. Configure Swift Package supported platform.
  4. Add Swift Package to the main project.
  5. Change access levels of classes and methods, e.g., private, public.

Create a new Swift Package

  1. Open your project and create a new Swift package by selecting File > New > Package....
  2. Named your new module. In this case, I named it "MyUI". Make sure you select your main project root folder as the destination and add the new package to the main project.

That's all you need to do to create a new local Swift Package for your module. Easy right?

Move files

After creating a new package, the next step is to move files over from the main project to the new package. But before we do that, let me introduce you to the sample project we will use throughout this article.

Here is our sample project. I have only Color extension in Colors.swift file. This is the file we want to extract into a separate module.

import SwiftUI

// MARK: - Color
extension Color {
init(hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 08) & 0xff) / 255,
blue: Double((hex >> 00) & 0xff) / 255,
opacity: alpha
)
}

// MARK: - Pink
static let pink100 = Color(hex: 0xf8bbd0)
static let pink200 = Color(hex: 0xf48fb1)
static let pink300 = Color(hex: 0xf06292)
static let pink400 = Color(hex: 0xec407a)
static let pink500 = Color(hex: 0xe91e63)
static let pink600 = Color(hex: 0xd81b60)
static let pink700 = Color(hex: 0xc2185b)
static let pink800 = Color(hex: 0xad1457)
static let pink900 = Color(hex: 0x880e4f)
}

And here is our content view, which shows all the colors we declared in the extension.

import SwiftUI

struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.pink100
Color.pink200
Color.pink300
Color.pink400
Color.pink500
Color.pink600
Color.pink700
Color.pink800
Color.pink900
}.ignoresSafeArea()

}
}

The result is this beautiful pink rainbow.

A sample app.
A sample app.

We move any class we want to extract into a module into a new package folder. In this case, we move Colors.swift from MyMain/UI/Colors.swift to MyUI/Sources/MyUI/Colors.swift.

Move files from the main project to the local package.
Move files from the main project to the local package.

Configure your Swift Package

If you change your Xcode scheme to the new package and build the project, you will get a lot of errors about Color availability.

You will get compile errors if you build your new package.
You will get compile errors if you build your new package.

By default, Swift packages are platform-independent. It means to be used by any platform you like, but we use Color, which is available only on some platforms (@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)), and that is the cause of the error.

We can fix this in two ways.
Use conditional compilation blocks to handle platform-specific code and achieve cross-platform compatibility.
Make Swift packages platform-specific.

Cross-Platform compatible

If you want your package to stay platform-independent, you must use conditional compilation blocks. In my case, I put @available(iOS 13.0, *) on my extension to make it available only for iOS 13 and forward.

This will fix the error we got earlier.

@available(iOS 13.0, *)
extension Color {

init(hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 08) & 0xff) / 255,
blue: Double((hex >> 00) & 0xff) / 255,
opacity: alpha
)
}

// MARK: - Pink
static let pink100 = Color(hex: 0xf8bbd0)
static let pink200 = Color(hex: 0xf48fb1)
static let pink300 = Color(hex: 0xf06292)
static let pink400 = Color(hex: 0xec407a)
static let pink500 = Color(hex: 0xe91e63)
static let pink600 = Color(hex: 0xd81b60)
static let pink700 = Color(hex: 0xc2185b)
static let pink800 = Color(hex: 0xad1457)
static let pink900 = Color(hex: 0x880e4f)
}

Platform-specific

If you only aim to support a subset of the platform and don't want to put conditional blocks everywhere, you can specify the supported platform in Package.swift file under the platforms key. This will make our Swift package become a platform-specific package.

I make the package only support iOS 13 and forward by specifying platforms: [.iOS(.v13)].

let package = Package(
name: "MyUI",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "MyUI",
targets: ["MyUI"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "MyUI",
dependencies: []),
.testTarget(
name: "MyUITests",
dependencies: ["MyUI"]),
]
)

You can make your package either platform-independent or platform-specific. The choice is totally up to you.

Add Swift Package to the main project

After we make sure our package is built successfully, let's switch back to the main project and run our app. You will get a compile error saying "Type 'Color' has no member 'pink100-900'".

Type 'Color' has no member error.
Type 'Color' has no member error.

That's because the newly created Swift package didn't automatically add as our project dependency. You have to do it yourself.

Add dependency to the new Swift Package

To add a new Swift package dependency:

  1. Select your project in the Project navigator, then select your app target and navigate to its General pane.

  2. Click the + button in the "Frameworks, Libraries, and Embedded Content" section, select the local package's library product, and add it as a dependency.

  3. Make sure your package shows up under the "Frameworks, Libraries, and Embedded Content" section.

Add import declarative.

Then you need to add the import directive just like other modules.

import SwiftUI
import MyUI

struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.pink100
Color.pink200
Color.pink300
Color.pink400
Color.pink500
Color.pink600
Color.pink700
Color.pink800
Color.pink900
}.ignoresSafeArea()

}
}

Change access levels

Our code still failed at this point, but with a different error. It will show "'pink100' is inaccessible due to 'internal' protection level" error.

Inaccessible error.
Inaccessible error.

By default, the access level for entities within your code is internal.

Internal access enables entities to be used within any source file from their defining module, but not in any source file outside of that module.

This access level is convenient for a simple single-target app where the code is self-contained within the same module.

Since we extract our code into two separate modules, the main project can't see the Color extension from the MyUI module. So, we need to make our extension public.

Change the access level of the Color extension to public and rerun the main app. This time, it will build successfully.

public extension Color {
init(hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 08) & 0xff) / 255,
blue: Double((hex >> 00) & 0xff) / 255,
opacity: alpha
)
}

// MARK: - Pink
static let pink100 = Color(hex: 0xf8bbd0)
static let pink200 = Color(hex: 0xf48fb1)
static let pink300 = Color(hex: 0xf06292)
static let pink400 = Color(hex: 0xec407a)
static let pink500 = Color(hex: 0xe91e63)
static let pink600 = Color(hex: 0xd81b60)
static let pink700 = Color(hex: 0xc2185b)
static let pink800 = Color(hex: 0xad1457)
static let pink900 = Color(hex: 0x880e4f)
}

And this is the final step. You just finished modularizing your app.

The final result of the app.
The final result of the app.

You can easily support sarunw.com by checking out this sponsor.

Sponsor sarunw.com and reach thousands of iOS developers.

Conclusion

Breaking part of your app into a module is straightforward with Swift Package. It only contains five steps, as we learned in this article.

And we can start to see the benefit of modular programming with just a simple app like this. Our MyUI becomes self-contain; we can focus only on the UI element in that module, and business logic from the main app can't leak into our UI.

The best part is you can incrementally migrate your code into module file by file. So, you can do it at your own pace.


Read more article about Development, Swift Package, Module, iOS, or see all available topic

Enjoy the read?

If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.

Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.

If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.

Become a patron Buy me a coffee Tweet Share
Previous
How to add Lint rules in Flutter

As your team and project grow, you might need to enforce some coding rules to make your code go the same direction. Luckily, adding custom lint rules in Flutter is very easy. Let's learn how to do it.

Next
How to use Proxyman with Flutter

If you use Proxyman with a Flutter app, you might not see any traffic from your Flutter Project. Here is how to fix it.

← Home