Xcode at scale - Xcode at scale
This is the first post of a serie named iOS at Scale based on the next steps:
- Introduction
- Xcode at Scale
- Refactor MarvelClient - split client from logic
- Refactor DataProvidersKit - applying Iversion of Control
- Refactor DetailKit - Single data flow, states, type erasure and more
- Refactor Navigator - Back to simplest
- Refactor AppCore - we really need it?
- Dependency Injection - flexible and composable
- Extra ball: Cache made easy
- Extra ball: Introducing to Promises 1
- Extra ball: Introducing to Promises 2
- Final conclusions
If you already have this type of conflicts removed from your project, splited your project in modules (with any project generator), you may go to the STEP 2.
Based on the given Architecture, we can see the benefits of have our app splited in several Modules. We have forlders with code linked to a namespace, using the public
/private
/interal
modifiers with sense and creating different spaces for each team of developers or developer.
The first thing we’re going to do to allow our project scale without problems, it’s to reduce as many as we can xcodeproj
conflicts. This conflicts are the main base of pain when working on huge (and not so huge) teams. This conflicts are mainly produced by the simple reason of adding a file on two different branches. To avoid the usage of tricks and waste time solving conflicts that we can get out we are going to remove as many xcodeprojs
as possible (all of them).
This is a very common problem on our days, Xcode configuration and Xcode compile times at Scale are known as not the best option, companies like Facebook or Google have developed other build systems (Buck, Bazel) with features like project generation, and reducing compile times. Those tools could be hard to setup, and if we don’t need super fast compile times, we’re going to try to remove at least the xcodeproj
conflicts using an provided tool, Swift Package Manager.
SPM (and any dependency manager tool (cocoapods, carthage…)) allows us to create small, reusables libraries (we have the limitation to build only static libraries with SPM). We not only are going to remove some xcodeproj
pain, we are winning the option to share our small libraries along all our company. Have our libraries like the http client
shared, allows us to versioning our library, share the same DSL along our developers, and solve bugs in one time for the whole company.
This also give us the option, if we want or our company allow us to, the hability to open source it.
To define our library as an SPM package, we only need to create a Manifest
file, called Package.swift
and specify where the code and the test are, along with the product that it builds. As any other dependency manager we can tag to specified version (to avoid the need of refactor code if we don’t have time), and gain the option to share it not only on iOs, also on Mac, TV and Watch apps.
So, I’m going to transform all the project modules into SPM, add it as dependencies to our App projects and then, create a Workspace that allow us to work on all of them at the same time.
Note: On big teams, maybe shared libraries it’s not the best option to be edited by the whole company, could be a good option have them in other repository and work on them when needed.
SPM Steps.
All of this steps are under git commits, so you can see all the sub steps that I do.
The first thing I did its move all the modules of the app to folders, create the packages (not added dependencies yet), we’ve archived to have a cleaner folder project, but we keep all the modules under excelsior
folder, that way it’s not so clean. The next step, it’s to move all this modules under a Module
folder, this way, with a simple look to the project on github or the folder inside your system, we may see the project structure fast.
If you go to the commit 15e96ed
(git checkout 15e96ed), you will se how clean it’s currently the project folder. This is extreamly useful when wee see the code on github for the first time, and help us to know how the code is distributed. Also, allow us to take a folder inside the project and in a few steps we have a separated library, ready for reuse (take care of dependencies). So, now, we only reorganized code into more illustrative folders.
With the Manifest
created, it’s time to link the projects each other, and try to make the app compile again.
Due to I’m not the creator of the project, I’ll create every project with swift package generate-xcodeproj
and checking its dependencies to link needed libraries.
…
After some time trying to do this, we will find that SPM doesn’t fits well for this use case.
This project has view in .xibs
and .storyboards
and resources like images, since SPM only builds static libraries, we can’t add any more than code. If we don’t do all our views by code (SwiftUI, Texture) and we don’t have images, SPM doesn’t not Support our Scale (hopefully yet). So we need findout a good solution to cover our use case.
But we’ve gained one main thing. Cleaner folder organization. And yes, this will help us a lot on the next solution.
We need a tool that avoid xcodeproj
conflicts, allow us to have different modules, with different dependencies, have different Manifest files for each of our projects and if it could be possible written in swift.
Do you know what tool we need right?
Tuist
Tuist its a tool that allow us to make our project by modules, generate the xcode on the fly based on Manifest files, and so much more that you cand find on the docs.
We’re going to create the needed manifest files called Project.swift
. Note that Tuist also offer us ProjectDefinitionHelpers
that allow us to extend his code base to add functions and simplify our configuration. Making our modules following the same structure that SPM follows under Creating a Library Package
, will simplify all our configuration in a very easy and understanding way.
First, lest create our ProjectDefinitionHelper for all our modules, specifing the main target and the test target. This files must be under
Tuist/ProjectDefinitionHelper`.
Our helper to create modules will look like this (you can find it on commit 4645d6b
).
extension Project {
public static func module(name: String, dependencies: [TargetDependency] = []) -> Project {
Project(name: name,
targets: [
Target(name: name,
platform: .iOS,
product: .framework,
bundleId: "com.excelsior.\(name)",
deploymentTarget: .iOS(targetVersion: "10.0", devices: .iphone),
infoPlist: .default,
sources: ["Sources/**"],
dependencies: dependencies),
Target(name: "\(name)UnitTests",
platform: platform,
product: .unitTests,
bundleId: "com.excelsior.\(name)Tests",
deploymentTarget: .iOS(targetVersion: "13.0", devices: .iphone),
infoPlist: .default,
sources: "Tests/**",
dependencies: [
.target(name: "\(name)"),
])
])
}
}
Next step.
We need to create the manifest file to all our projects.
Our AppCore
manifest file will look like:
import ProjectDescription
import ProjectDescriptionHelpers
let project = Project.module(name: "AppCore")
I think more simpler it’s impossible. This will generate our xcodeproj
on the fly xcodeproj
without effort. (commit 94ecf97
).
Now let’s generate the rest of the modules Manifests
. You can find this done at 7ca2a96
It’s time to get out our App project xcodeproj
. Our App project Manifest will look a little bit different, but not too much. In order to do that, I’ll move all the App files under a folder called App
so we again gain a cleaner files and folder structure.
All these changes can be found on commit fd92561
.
Now I like to create a workspace with our app (all the project dependencies projects, not linked yet, will be generated also when they’re linked).
The Manifest for the project has the name Workspace.swift
and will look like this: (commit: 3161c97
)
import ProjectDescription
let workspace = Workspace(name: "Marvel",
projects: ["App"])
Okey, it’s time to generate our project (command: tuist generate
). After fix a little error on UITest sources declaration on App/Project.swift
we end up with the Excelsior App project generated. hash 90e4180
.
Now its time to link our dependencies, before of do this, I’m going to create another ProjectDefitionHelper
to name my dependencies and then have the hability to add them with the swift dot notation .dependencyName
. This is extreamily useful, because we get abstracted of how our dependencies are provided. We don’t care if the dependencies are given by Projects
, Carthage
o even Packages
. Note that project dependencies will be added to the workspace too and we can work on that while packages, carthage or cocoapods dependencies not
Lets introduce here the most useful Tuist command. tuist edit
. this command allow us to edit the project configuration files inside xcode (more info here). So we will run tuist edit
under excelsior
folder and it will open up the workspace with the project definition helper folder (what it’s the folder that interest us now).
Our dependencies helper will look like: (commit: 7a37167
)
import ProjectDescription
public extension TargetDependency {
// App
static let appCore: TargetDependency = .project(target: "AppCore", path: .relativeToRoot("Modules/AppCore"))
// Features
static let characterDetailKit: TargetDependency = .project(target: "CharacterDetailKit", path: .relativeToRoot("Modules/CharacterDetailKit"))
static let characterListKit: TargetDependency = .project(target: "CharacterListKit", path: .relativeToRoot("Modules/CharacterListKit"))
// Core Dependencies
static let dataProvidersKit: TargetDependency = .project(target: "DataProvidersKit", path: .relativeToRoot("Modules/DataProvidersKit"))
static let displayKit: TargetDependency = .project(target: "DisplayKit", path: .relativeToRoot("Modules/DisplayKit"))
static let marvelClient: TargetDependency = .project(target: "MarvelClient", path: .relativeToRoot("Modules/MarvelClient"))
static let navigatorKit: TargetDependency = .project(target: "NavigatorKit", path: .relativeToRoot("Modules/NavigatorKit"))
static let support: TargetDependency = .project(target: "Support", path: .relativeToRoot("Modules/Support"))
}
Now let’s link our dependencies. To do this, I’m going to start from the App target, trying to compile it, and then going resolving dependencies. Out app don’t compile why we dont have AppCoreKit dependency.
At this point I’ll add some notes:
- I’ve removed the suffix kit on the organizing project so now we need AppCore
- I’ll be renaming things only to fell more confortable
- On code steps, I’ll rename some things in order to follow industry standards, and I’ll add documentation about why when I do it
We change the AppCoreKit to AppCore and add the dependency to the App/Project.swift
with dependencies: [.appCore]
. After generating the project we will find out that our Workspace now have a Group called Modules
with our AppCore
inside (hash 4f2ef3b
).
Now, like this, I’ll resolve all dependencies and try to compile the whole app. Note: I previously deleted necesary Headers files, in the next commit you will see them in again.
Finally, after link all dependencies, I’ve added ResourceType
to Tuist ProjectDefinitionHelpers, check that the app compile, tests are passing and everything it’s working again.
You can check the code at this commit ccf6155
. And yes, Kingfisher it’s commented, so it’s time to add the dependency. And end with the project settup.
Then, after add Kingfisher by SPM to the DisplayKit (previously CommonUIKit
), we got it available on all projects that have DisplayKit as dependency.
Our project it’s finally in what a think, a good initial state.
- Folders organized
- Removed xcodeproj conflicts, we don’t need to use tricks anymore
- The only dependency given by SPM (but you can use whatever dependency manager you want)
- From github the code will provide us an easy way to know how the app it’s defined
- Maintaining the whole workspace where we can edit our code
(commit: 040bd43
)
To end with this chaper, the last thing we’re going to do it’s remove the xcodeproj
and workspace
from the repository, and also we will ignore the Tuist Derived folder. Now the project could be easy generated by tuist generate
. (commit: a1f95eb
)
Clonclusion
We’ve seen how SPM actually doesn’t fits our need by the moment, but how some other tools like Tuist could help us removing conflicts and making our interactions with Xcode easier.
We have all the Xcode realted configurations inside swift code, and not inside the xcode project.
Also we’ve done a simple folder organization to have a clearer way about where is our code situated. Making us easy to understand how it’s built.
Steps
- Introduction
- Xcode at Scale
- Refactor MarvelClient - split client from logic
- Refactor DataProvidersKit - applying Iversion of Control
- Refactor DetailKit - Single data flow, states, type erasure and more
- Refactor Navigator - Back to simplest
- Refactor AppCore - we really need it?
- Dependency Injection - flexible and composable
- Extra ball: Cache made easy
- Extra ball: Introducing to Promises 1
- Extra ball: Introducing to Promises 2
- Final conclusions