Adding features to a large, existing codebase is a challenge that many engineers will face in their coding career. If an iOS app has been around for more than a few years, it’s likely to have had many developers contributing on many features that made it into many releases. Often, the result can be a very large codebase with varying degrees of modularization, long build times, and not very clean code.
The iOS app at Scout24 was in such a state not long ago, and I’d like to share with you how we made significant improvements to our development environment and what the benefits were.
If you’ve been developing with Swift in a large project, you are probably too familiar with the amount of time it takes to build the entire app module, even after a small change. There are tons of resources on how to improve the overall duration.
Swift has a lot of great language features, but those features come at the cost of longer compile times. Even after making a small change, you could be waiting for up to a few minutes before you can see the results. This is really distracting and very inefficient! Time and concentration are your most precious resources while coding.
How to solve it
The answer to the long build times is simple: make the compiler build less code! Sounds reasonable, but how can we achieve this? The answer is by using iOS frameworks to compartmentalize our code into smaller, more discrete chunks of functionality.
In this example, we will start with the main app module and add a framework that contains some UI functionality that makes up a feature which we will integrate into our app.
What are the advantages of this?
It may not seem obvious with such a small example, but the advantages of developing in this way are huge. They are especially big when you have to build functionality into a large app that has a lot of legacy components.
By building features as separate modules that are highly independent of the main app module, you can focus more on the features you want to create. You will notice being forced to organize your code in a way that exposes only what is necessary to the app module, the code tends to be much cleaner and well thought out, and it’s less likely to develop into a spaghetti mess because it’s more difficult to do so.
An essential step in building features as frameworks is to create a “wrapper app”, much like we will do in this tutorial, that minimally integrates the functionality you’ve created into a tiny app. This app won’t have to build all the other dependencies necessary for your main app module, so your build times will be lightning fast.
Create a new app workspace
You will need to create a workspace for your app and the framework where your feature will live. Start by clicking File -> New -> Workspace in Xcode. Create a new folder where you want to save the workspace.
After saving, we now have a blank workspace. Now click File -> New -> Project in Xcode and select “single view app”.
Name your project “SimpleCounterApp” and make sure “Swift” is selected as the app language. It’s named like this because it will just include the app module.
At the next step, select the folder where the SimpleCounter.xcworkspace is saved. Under “Add to” select the SimpleCounter workspace. SimpleCounter.xcworkspace has to be opened in Xcode to show this option.
Create the project and notice now we’ve got a project that looks pretty boring.
Let’s get to making a framework! We will integrate our feature into the main app in a later step, so there’s no need to do anything now with it.
Click File -> New -> Project again and this time, select “Cocoa Touch Framework” as the template.
Name the framework SimpleCounterFeature and click next. On the next screen, select the folder where the .xcworkspace file is saved so that the framework and app are in the same root directory. Then, under “Add to”, again select the SimpleCounter workspace. Under “Group” select the root workspace node.
This will add the framework to the workspace, rather than the app module. Save the project. You should now have a workspace that looks like this:
In the schemes list, you should also have two schemes, one for the framework and one for the app.
Because this framework template is totally bare-bones, it doesn’t come with any dependencies linked with it. So we need to add the basics, starting with UIKit.
Navigate to the project settings for the framework and select Build Phases. Under Link Binary with Libraries, click the + button and link the UIKit and Foundation frameworks. Your build phases screen should look like this:
Now, let’s add a simple view controller with a nib to the framework. Click File -> New -> File and select “Swift File”. Click “Next”.
Name the file CounterViewController and add the file to the group SimpleCounterFeature. This will not be selected by default.
Now you have a file that is part of the SimpleCounterFeature framework target. Do the same thing again with a nib file (“View” template in the “New File” dialog), making sure to add it to the SimpleCounterFeature group.
Exposing functionality from your framework
When writing code that you want to expose to the app module from your framework, it’s important to keep in mind what access level you give your interfaces. We don’t really concern ourselves normally with public, open or internal access levels when writing Swift code for an app module. That’s because all the code lives in the same module, making the distinction less important. In a module, internal is the default access level for member properties and methods, meaning they are available across the module by default.
When developing frameworks, it’s different. Because the code you write will be packaged into a separate binary to be used by a module outside its scope, access levels are more relevant.
If you want to expose anything to the outside, whether it’s a struct, protocol, class, or any property or method, you need to specify a public access level. If you don’t, the entity will not be visible to the module that imports it. The Swift docs have a great explanation about access control that you may read a bit differently having completed this tutorial.
For example, in our CounterViewController class, we need to expose the class to be public if we want to be able to use it outside the framework module:
The member IBOutlet properties will not be visible outside the module since they are marked as internal.
I won’t go into detail about how to wire up the nib to the view controller, or how to add the rest of the counter functionality, as there’s nothing special here. If you’re interested, you can check out the sample project to see how it’s done. In interface builder, I just created a button and a label and hooked them up to the IBOutlets and added some simple functionality.
Adding the framework to the app module
Now go to the General section of the project settings for the app module. Under Embedded Binaries, click the + button and add the SimpleCounterFeature.framework.
This adds the framework to the Embedded Binaries for the app module and links it. Adding the framework to the Embedded Binaries is needed because your SimpleCounterFeature framework isn’t available to iOS like UIKit is, meaning it needs to be copied into the app bundle during build time.
Now build the SimpleCounterApp scheme. If it built successfully, you are ready to start integrating your framework!
Integrating the framework into your code
Before we integrate the view controller into your app module, we need to do some cleanup first. Start by deleting the Main.storyboard file and deleting the Main Interface entry in the General tab of the app module’s project page.
You can also delete the ViewController.swift file in the main app module, too. You won’t be using this.
In the AppDelegate.swift file, import the SimpleCounterFeature framework and create a UIWindow with the root view controller as the CounterViewController.
Now, build your app. It should build successfully and launch with the CounterViewController as the root. You’ve done it!
Want to do more?
We left out something that developers almost always need in their codebase: 3rd party dependencies. I’ve written a tutorial on how to integrate them into your frameworks as well as provide a more complex code example in the next article.
- Example code: https://github.com/akfreas/SimpleCounter
- Apple Docs on Access Control: https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html
- How to use CocoaPods with your internal iOS frameworks: https://medium.com/@akfreas/how-to-use-cocoapods-with-your-internal-ios-frameworks-192aa472f64b
Thanks for reading!
If you have any questions, comments or requests for future articles, please leave a comment below!