This post is a tutorial to setup a multi project iOS application workspace. It doesn’t necessarily need to be an iOS application though, this setup can be used for macOS, tvOS, or a mixture of those platforms to share some of the code base.
So what’s the purpose?
- This approach will allow you to think in terms of modules. Separating your project into multiple modules will allow you to design better abstractions.
- It will also help you to save time in testing, since every module will be treated as a separate product and they will all have their own set of tests.
- Another benefit is the reusability. Every module can be reused in multiple projects. Saving us time in both during development and maintenance.
Throughout this tutorial, we will be using Xcode 9.3. There’s a slight difference in multi project setups if you’re using Xcode 9.3 or an older version. I’ll be pointing the exact step when we get there.
Let’s get started!
We will begin with an empty workspace. Create an empty Xcode workspace and place it in a directory of your choice.
So I’ve created a directory named
MultiProject on my Desktop and created a workspace with the same name in that directory.
And this is the workspace itself.
It has nothing. No project, no scheme, no files, nothing.
Adding the application project
Now let’s create the project for the app of the next big thing.
Again, we’re using the
File menu of Xcode. When we select
Project, we will be greeted with this screen.
The window in the background is not our workspace but we will get there in a moment. Let’s stick with the basics and select
Single View App. In the next screen fill in the details for the app and continue to the screen where we choose the directory for our project.
Here’s the part that we add this project to our empty workspace. Instead of creating the project in another directory, just open
Add to drop-down menu and select our workspace. As a side note, for the workspace to show up in this menu, it must be already open.
And the moment we select our workspace, both the directory for the new project it’s showing becomes the directory that our workspace is in and also the
Group drop-down menu shows our workspace meaning the new project will be placed in the root of our workspace. Just click
Create and you will see the project being closed but instead is opened in the workspace.
If we take a look at what we have right now, we have a workspace which has a project which has a target for the iOS app, a target for unit tests, and another target for UI tests. As we created the project in the workspace, the scheme for the app is also added as an option. This looks exactly as if we created a project and opened it.
Adding the framework project
Let’s make this look more like a workspace by adding another project. We will follow the same steps as we added the app project, but this time we will add a framework instead.
On project creating step, make sure to select our workspace in both
Add to and
Group drop-down menus.
This will place the directories for both the app and the framework side by side in the root directory. After clicking
Create now we have this.
Now this looks like a workspace. 😄
Framework test run
So far so good! Let’s create a new class for our lovely
AwesomeAPI framework to use in the app target.
Here I have a class named
AwesomeAPIClient which is a singleton object and has a function named
testRun. Notice how each component is declared
public. Since this class will be available in
AwesomeAPI framework, we need to declare every component that we want to access from the app target
Linking the framework to the app
Onto the app side. Let’s use the awesome framework in our app!
First we need to link the framework to the app target. But also we need to embed it to the app target as well, so that it will be bundled within the app when we build the app, making it available to use as if it’s a system provided framework.
On the project overview screen, just press the
+ button in
Embedded Binaries section to show the above sheet. From there, select
AwesomeAPI.framework and since the reason for embedding a framework is so obvious, Xcode will add it to the
Linked Frameworks and Libraries section as well.
Notice the framework added to the project group in Project navigator. Before Xcode 9.3, we had to add a reference to the
AwesomeAPI project inside the app project to make it visible to be linked and embedded. Now, Xcode 9.3 uses a different project and workspace structure (see the
Project Format listing in File inspector on the right pane) that allows us to use other targets in other projects within the same workspace.
Unfortunately, Xcode doesn’t give us all the goodies at once.
¯\_(ツ)_/¯ This leaves us with a dangling framework inside the project group and the down side is, you can’t just move it to another group. You can try creating a
New Group without Folder and moving the framework to it. It’ll just create another reference which is useless (it’s not the same reference that’s linked and embedded even).
If you’re living an OCD-free life, you might be like ‘okay, whatever’ with this kind of a structure. Another way is the old-fashioned-way I mentioned above. We'll do the things as we would do with previous versions of Xcode. First, right-click and remove the framework from the app group. This will also remove it from
Embedded Binaries and
Linked Frameworks and Libraries section as well. Second, just create a group without a folder (preferably with the name
Vendor) inside the app project group and drag-and-drop the
AwesomeAPI project in it.
Make sure to have the
Copy items if needed unchecked. And finally, to link the framework, click the same
+ button in
Embedded Binaries section to show the framework selection sheet.
Pay attention to the list above, we have two listing of the same framework. Make sure to choose the one within the app project group. This way we won’t have any additional framework references.
Xcode 10.2.1 Update
I’m not sure exactly when the aforementioned issue has been resolved but as I recently tried with Xcode 10.2.1, we don’t have to have a nested project anymore. And it’s also easier to maintain the project structure too.
In the above screenshot, notice that Vendor group is empty, meaning that nested project is removed. Also I have a reference to AwesomeAPI.framework within the app’s project. Following the same steps above, after adding the framework, we can drag and drop the framework under any group while holding down the ⌘ command key. This bit is important, because if we don’t use the ⌘ command key, it’ll copy the reference (basically same problem above).
After having a proper reference, the rest basically the same. So you’ll see the nested project in the rest of the article, but regardless which way you consume the framework, result will be the same.
Run the test run
All is well! Let’s use our framework in the app! Open the template
ViewController class and modify it as follows:
We imported our framework and called the
testRun function on
AwesomeAPIClient singleton in
viewDidLoad function. Let’s run the app and we should see the following output in the console:
AwesomeAPIClient is up and running! 🎉
Dependencies are awesome! (Okay, at least most of it. 😅) And we have two popular options to integrate them to our projects. CocoaPods and Carthage. Both of them have their pros and cons, lovers and haters. I have been using CocoaPods for years, but recently, especially with this setup, I have started to use Carthage and I think it fits very well for our case too.
With this kind of a setup, we can have many frameworks. Frameworks containing several reusable parts and layers of our application. Networking, UI components, persistence, analytics, extensions that we implement the extra stuff that should be already implemented in the language itself, and many more. In other words, the groups that you have in your application project. Each one of them can be a separate framework letting you reuse them in another project. Now you may think how can you reuse a networking layer or analytics logic that are so specific to your app. Well, at least you can have them contain the base implementations. For example you can make the networking framework free from any API URLs and make the app utilize it.
Why Carthage fits well with this setup? Imagine having many projects in our workspace. And two of them may need the same dependency. With Carthage, we can have this dependency be built in the root directory of our workspace and simply link it with both of the projects. But since CocoaPods takes a single project as an input and manipulates the project and adds it into a new workspace, it’s simply not possible for this setup (unless I’m missing some usage tips). Even if we could utilize CocoaPods, it just doesn’t make sense to maintain two or more separate
Podfiles for a given dependency.
Without further ado, let’s setup our dependencies. Since
AwesomeAPI is our gate to the interwebs, it’s a high probability for us to use another awesome framework, Alamofire!
To keep things tidy, it’s a good idea to keep all of our Carthage setup in a separate directory,
Vendor. Inside this directory, let’s create a
As per the documentation of Carthage we need to run
carthage update in the same directory. If you want to limit framework builds for only iOS we need to add
--platform ios parameter to this command.
Vendor/Carthage/Build/iOS directory, rests our lovely
Alamofire.framework which we can simply drag-and-drop to our project.
Again I created a new group without folder reference named
Frameworks and dragged-and-dropped the
Alamofire.framework in this group which led me to the above screen. Also again, make sure to have the
Copy items if needed option unchecked. This will allow us to reference the same framework whenever we update it with Carthage. Click
Finish and notice the framework is already added in
Linked Frameworks and Libraries section.
Up the game with test run
AwesomeAPIClient class to make a sample request in
No such module ‘Alamofire’
What’s this about? We already linked the framework. Why can’t it find the module? That’s because Xcode doesn’t know where to find the framework. To fix this select the project from Project navigator and while the framework target is selected navigate to
Build Settings tab. In the search bar type ‘Framework search paths`.
This is the field that we need to tell the directories our frameworks reside to Xcode. Since we need the same information for every configuration we have (Debug, Release, etc.), just double click and enter the Carthage iOS build directory path.
PROJECT_DIR variable points to the directory
AwesomeAPI project is in. For more information about build settings and available variables, please refer here.
With the path set, select the
AwesomeAPI framework scheme and confirm it builds successfully.
Let’s add a request to
I just took this sample from the Alamofire documentation. To make sure the request is being sent and we get a valid response, we can add a simple test case for this class. In the end, we are all good developers who write tests, right? RIGHT? 👀
Here’s a very basic test. But we need one more thing to do. Since test target is a separate build than our framework, we need to make sure it’s embedding
Alamofire.framework as well. You may think that these are all very cumbersome when using Carthage but trust me it’s not. You just need to get a grasp of it and you will be doing all this setup at most once per project. After that, all is well!
Select the project from Project navigator and while the test target is selected navigate to
Build Phases. Here, click the
+ button on the top left corner and select
New Copy Files Phase. This will add a new entry. You can rename it from
Copy Files to
Copy Frameworks as I did above, since it will be responsible for framework copy task only. Choose
Frameworks from the
Destination drop down menu and using the
+ button under the list, add
AwesomeAPI scheme is selected and watch as your test succeeds.
Perfect! Let’s use it in the app as well
ViewController class and you should see an error immediately (or at least when you try to build the app).
This is obviously because we modified
testRun function. Let’s reflect the changes.
For the sake of this tutorial, we there’s not any data passed in, but
testRun function has still
But instead we get an immediate crash.
dyld: Library not loaded: @rpath/Alamofire.framework/Alamofire
Referenced from: /Users/ilter/Library/Developer/Xcode/DerivedData/MultiProject-attxtmjkzrjkqibyrvrfpwmyfhby/Build/Products/Debug-iphonesimulator/AwesomeAPI.framework/AwesomeAPI
Reason: image not found
But why? We are using
Alamofire in our
AwesomeAPI framework already? What’s wrong here?
The answer is,
AwesomeAPI framework uses
Alamofire framework. We linked
Alamofire framework to
AwesomeAPI framework and
AwesomeAPI framework is both linked and embedded to our app target. But embedding a framework to an app target does not bring the other frameworks that are linked to the embedded framework along with itself. Since
Alamofire is not a framework that ships with iOS, we need to embed it to our app target too. In the end we will have a setup as follows.
Alamofire frameworks both will be the dependencies that we ship with our app. Frameworks can’t embed any other framework but since our app target is capable of embedding frameworks, we need to embed
Alamofire too, even though we’re not directly using it.
Alamofire we’re going to do something slightly different from what we did while embedding it into
AwesomeAPITests target. The detail here is, as we have used Carthage to download and build
Alamofire framework, it contains all the binary slices which includes iOS (arm64, armv7, and armv7s) and iOS Simulator (i386). It’s totally okay to have all slices in the framework shipped with the app, it will work. But you will see an error from iTunes Connect when you try to upload your app to the App Store. It will warn you to remove unused slices (in this case, i386 architecture slice, since none of the simulators will download our app from the App Store). Carthage is ready to help us with that.
Switch to Build Phases tab in the app’s project and add another Run Script Phase. I renamed it to
Carthage (Copy Frameworks). Here we’re using Carthage to copy the frameworks which is only copying the necessary slices. As input we’re pointing to
Alamofire.framework in our Carthage build directory.
And as the output we’re pointing to the
Frameworks directory inside the application bundle.
The path to
Alamofire.frameworkshown above is
You can find the framework in its place after building the app once more.
$FRAMEWORKS_FOLDER_PATHare all predefined variables in the environment added by Xcode.
Let’s build and run now.
Voila! As we see the expected console output, all is well.
To sum up what we have done until now:
- Created an empty Xcode workspace
- Created an Xcode project for our iOS app and added it to the workspace
- Created an Xcode project for our framework and added it to the workspace too
- Linked and embedded the framework to the iOS app and used it in the view controller
- Added a dependency with Carthage
- Linked the dependency to the framework
- Embedded the dependency to the iOS app using Carthage
You can have as many separate projects as you like and all of them can be consumed by your app target. You can have as many dependencies as you want and multiple projects can easily consume them too. Your dependencies not necessarily need to be linked to a framework. You can have one or more dependencies consumed by both frameworks and app targets. You can mix and match however it fits your needs.
You can even use your own frameworks in other projects too. For example you can have a separate repo for a framework and add it to many workspaces and repos either using Carthage or git submodules. And if you add frameworks prebuilt with Carthage you can shorten the build times for the app as well. It’s also helpful in terms of tests. You don’t need to run all of the test suite just for a change in one of the frameworks or the apps. This setup is very flexible and provides many benefits.
You can find the sample repo here.