Better iOS projects: Getting (nearly) rid of .xcodeproj-Files - A (not so) short Introduction to Xcodegen
In the series “Better iOS projects”, we have a look at the various tools and environments that are useful to have a more convenient and efficient handling of iOS projects.
July 24, 2018, by Wolfgang Lutz(Twitter, GitHub)Updates
- 23. February 2021: Small updates (e.g. SwiftUI) and additional integrations (CocoaPods, SPM)
- 28. July 2018: Removed
brew tap
call, as it is no longer necessary.
What is the problem with .xcodeproj
s?
Xcode uses a project file, the .xcodeproj
file, to bundle source code and resources for the IDE and
build tools to digest. Though this works quite well most of the time, it has some downsides:
- Though you can manually resolve merge conflicts in this file, e.g. if source code files or resources have been added on different branches, you can never be sure that the file is correct afterwards.
- Syncing the folder structure on disk and the group structure in the project is mainly a manual process that
sometimes leads to confusion. There already are tools to mitigate this, like
synx
or the sort functionality of thexcodeproj
gem. - Xcode does not warn you when a file is missing until you compile.
- Orchestrating dependencies and build scripts of multiple targets can become quite a hassle.
Introducing Xcodegen
Xcodegen is a tool, that allows us to generate the xcodeproj file from a definition in a file called project.yml. As the xcodeproj file can be generated whenever we like, we do not even have to keep it inside our git and can ignore it (though I personally prefer to keep it checked in, so I can see what changes my edits to the project.yml file introduced to the project).
Here are the two most important features of Xcodegen:
- You can define every kind of Xcode target (application, frameworks etc.) for all sorts of platforms (iOS, tvOS, macOS and watchOS) this way.
- It also allows to connect a folder of source files to a target, making it easier to manage which source code files are contained in which target.
Though XcodeGen is still quite a young project, it can already do a lot. Sometimes workarounds are needed, but the author is quite active on GitHub and bugs are often fixed only hours after reporting them. A big thanks for that!
How to install xcodegen
Amongst other methods of installation, you can install xcodegen using brew, by running
brew install xcodegen
or, if you are a loyal reader of this series, by using mint
mint install yonaskolb/xcodegen
Generating an App Project
First, create a new blank Single Page iOS App with Xcode to initially get all the necessary .swift
,
.xcassets
etc. files. In the project creation dialogue, select UIKit. Xcodegen works for SwiftUI
also, but the example in the end makes use of TinyConstraints, an AutoLayout Library, that is best demonstrated
using UIKit.
We will now recreate the project using a project.yml
file.
Leave Xcode
and create a project.yml
file with the following content in the root:
name: XcodegenApp # The name of the App
options: # Some general settings for the project
createIntermediateGroups: true # If the folders are nested, also nest the groups in Xcode
indentWidth: 2 # indent by 2 spaces
tabWidth: 2 # a tab is 2 spaces
bundleIdPrefix: "de.number42"
targets: # The List of our targets
XcodegenApp:
type: application
platform: iOS
deploymentTarget: "14.0"
sources:
#Sources
- path: XcodegenApp
Then, rename the existing .xcodeproj
(so that you can have a look at it
and compare). I always just add “Backup” to the name here.
In the terminal, in your project root, run xcodegen
or mint run
xcodegen
:
Open the project and run it. You have the same results as before!
“The same results? But what about testing? The tests are gone!” I can hear you say. Do not worry, we will fix that immediately.
Generating TestTargets
Add the following target to the project.yml:
XcodegenApp-iOS-Tests:
type: bundle.unit-test
platform: iOS
deploymentTarget: "14.0"
sources:
- path: XcodegenAppTests
dependencies:
- target: XcodegenApp
Again, close Xcode
. (Xcode is a bit peculiar about having changed the
project files while they are open and gets angry sometimes.)
Generate the project, open it and run the tests: voila!
To add UI Tests, add this target:
XcodegenApp-iOS-UITests:
type: bundle.ui-testing
platform: iOS
sources:
- path: XcodegenAppUITests
dependencies:
- target: XcodegenApp
That’s it, a buildable app project with working tests.
Generating a Framework Project
Let’s go a bit deeper now:
Maintaining submodules in Xcode has always been a bit of a hassle.
Introducing modularization into your app is a breeze with xcodegen
.
To learn how to do this, we create a XcodegenAppCore framework, that contains the classic fruit enum:
-
Create a folder “XcodegenAppCore” in the root
-
Create “Fruit.swift” inside this folder. Add this as content:
public enum Fruit: String { case apple case banana case cherry }
-
Add an Info.plist with these contents to the
XcodegenAppCore
folder:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
-
Add this target to the
project.yml
:XcodegenAppCore: type: framework platform: iOS deploymentTarget: "14.0" sources: - path: XcodegenAppCore
-
Add
dependencies: - target: XcodegenAppCore
To the
XcodegenApp
target. The result should look like this:XcodegenApp: type: application platform: iOS deploymentTarget: "14.0" sources: #Sources - path: "XcodegenTest" dependencies: - target: XcodegenAppCore
Close Xcode, run
xcodegen
, then run the project. Ok, it’s building, but nothing is happening yet. -
To test the framework in an UIKit app, add
import XcodegenAppCore
to a ViewController and add
print(Fruit.apple)
to the
viewDidLoad()
.
If you created a SwiftUI project, you can change the view to this to test the framework:
import SwiftUI
import XcodegenAppCore // import the framework
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.padding()
.onAppear {
print(Fruit.apple) // Use something defined in the framework when the view appears
}
}
The debug logger now proudly confirms you as the owner of your very own fruit related framework 🍎 🍌 🍒.
Adding Dependencies
CocoaPods
You can simply integrate the generated project with Cocoapods, like you would integrate any other project, just
make sure to call pod install
after every time you run XcodeGen.
Swift Package Manager (SPM)
XcodeGen has direct support for Apple’s own Swift Package manager.
- Add
packages:
TinyConstraints:
from: "4.0.1"
url: "https://github.com/roberthein/TinyConstraints"
at the same level of the project.yml
as the targets
section.
- Add this line to your
XcodegenApp
target’s dependencies inproject.yml
:
- package: TinyConstraints
Close Xcode, run xcodegen
, then run the project.
Carthage
Yet another way to handle dependencies on iOS is Carthage.
XcodeGen makes it very easy to manage Carthage dependencies, so let’s add roberthein’s TinyConstraints as a Layout Library to learn how that’s done:
-
Run
brew install carthage
-
Create a file named “Cartfile”
-
Add
github "roberthein/TinyConstraints"
to it.
-
Run
carthage update --platform iOS
-
Add this line to your
XcodegenApp
target’s dependencies inproject.yml
:- carthage: TinyConstraints
Close
Xcode
, runxcodegen
, then run the project.
Testing the new Dependency
Whichever dependency manager you chose, you can now use TinyConstraints:
import TinyConstraints
In the viewDidLoad(), add:
let fruitLabel = UILabel()
fruitLabel.text = Fruit.banana.rawValue
view.addSubview(fruitLabel)
fruitLabel.centerInSuperview()
Run the project to see TinyConstraints in Action: Acknowledgments
Thanks to Yonas Kolb for reviewing this article before release and to Maximiliane Windl for testing the tutorial, fixing issues and making screenshots. Also thanks, as always, to Melanie Kloss for the great banner image.