Mature iOS Development 2 - Xcode Dependency management

This is the second article in a series of blog posts on tips to improve your iOS development process.

Chances are that if you are developing an iOS project, you are going to make use of third party libraries or code. If you are not, then you should. There’s no need to reinvent the wheel. There’s a wide community of developers out there working towards solving similar problems, and sharing their solutions.

I actually have a set of libraries that I always include in my projects right from the start. The ones I use the most are the following:

  • AFNetworking (networking library)
  • ReactiveCocoa (functional reactive programming framework)
  • Objection (lightweight dependency injection framework)
  • Kiwi (behavior-driven-development framework)
  • KIF (functional test framework)

If you build a project relying on any of those frameworks and libraries, your project won’t run without them. That’s why they are considered dependencies in your project. Dependency Management is the process to help you manage those dependencies (adding, removing, switching versions). Usually that process is handled by an additional tool.

Compared to other languages and programming environments, Xcode and iOS/OSX development in general doesn’t have that many options for dependency management tools, and in my personal opinion, the existing ones are not as powerful as tools like Maven or Gradle for Java.

Some of the different dependency management tools (just for the sake of listing, haven’t tried most of it):

  • Cocoa Pods: Biggest community and largely supported (my preferred one).
  • Xcode Maven: Powerful, especially if you also want to use it as a build system.
  • Xcode Gradle: Very similar to Maven in terms of features, but less verbose to setup.
  • Carthage: Less opinionated and more flexible tool. Biggest advantage is that it compiles dependencies as a .framework, and it uses Xcode build system to include them in yours, but you have to include them manually.

I have a strong preference for CocoaPods. It’s not the perfect tool, nor the most flexible, but I tend to go through the path of least resistance. CocoaPods is the simplest to setup and start using, and also the most supported. It also plays nice with continuous integration. If you aren’t using any dependency management today, I recommend starting with CocoaPods, and that’s the one I will be focusing on in this blog post.

I won’t be focusing as much on installation or the basics on how to use, since there are numerous sources already available for that, including CocoaPods Getting Started, and this detailed blog post. I will be focused mostly on describing how it can potentially improve your development process, and how it works.

Benefit #1 - Simply put: Handling Dependencies (and keeping it clean and organized)

If you haven’t used it, follow CocoaPods Getting Started. Once done, create a new project (include unit tests for the sake of following this blog post), and from the root folder of your project, run both commands:

pod init
pod install

This will generate a Xcode Workspace with 2 projects in it. One project will be the project you will be coding your application on, the second project is the project used by CocoaPods to package your dependencies. It will also put a Podfile file in the root of the project, that’s where you add your dependencies. Once you do that, you should not open your Xcode by using the same Project file, but by using the Workspace. (.xcworkspace instead of .xcodeproj)

Open Podfile, you should see something like the following:

# Uncomment this line to define a global platform for your project
# platform :ios, '6.0'

target 'RavelAntunesApp' do

end

target 'RavelAntunesAppTests' do

end

CocoaPods initialization will generates one block per target in your application for you to add dependencies. Since I created a project with unit tests, I have 2 blocks. That’s a neat way of making sure your dependency management process is adding dependencies to the right targets. For example, let’s add one dependency to our main target, like AFNetworking (you can find dependencies by using the search box on cocoapods.org. In the first target block add the following:

pod 'AFNetworking', '~> 2.5'

Your file should look similar to:

# Uncomment this line to define a global platform for your project
# platform :ios, '6.0'

target 'RavelAntunesApp' do
    pod 'AFNetworking', '~> 2.5'
end

target 'RavelAntunesAppTests' do

end

If we run pod install on command line, CocoaPods will download and include the dependencies in your project. Now, let’s add a second dependency called Kiwi to our project. Kiwi is a behavior-driven development framework, and I usually use it to help writing descriptive tests on my projects. Since I just use for testing, it’s not relevant to my main target, and I don’t want to include it. We can include it only on the test target by adding it to the second block: the Tests block. My file now is looking like the following:

# Uncomment this line to define a global platform for your project
# platform :ios, '6.0'

target 'RavelAntunesApp' do
    pod 'AFNetworking', '~> 2.5'
end

target 'RavelAntunesAppTests' do
    pod 'Kiwi', '~> 2.3'
end

After that, I need to run pod install again so CocoaPods can resolve the new dependency.

How is CocoaPods Making Those Libraries Available?

Let’s look at our project structure to understand what’s happening. Collapse the Pods project you will see the following:

image

First, pay attention to the Pods directory inside the Pods project, it includes both AFNetworking and Kiwi, and the content of those directories are the files for the libraries itself, that were downloaded directly from (most likely) a Git repository.

Second, look at the Frameworks/iOS directory, and you will see a few frameworks listed. Those are frameworks that AFNetworking and Kiwi depend on. This is probably one of the biggest values of using a dependency management tool. A dependency might have its own dependencies, which might recursively have more dependencies. When CocoaPods resolves the dependency for you, it handles the dependency’s dependencies. You don’t have to manually import or include the required frameworks.

Third, look at either the Products directory, or the list of targets on the right side. This is the final product of your dependencies. CocoaPods compiles each one of the dependencies into a static library. In addition to that, it creates another static library combining all the dependencies’ static libraries into a single static library, and then links that to the target that depends on those dependencies. I added another dependency (SVProgressHUD) to our main target to better show the result of that compilation.

See below, the 2 unselected targets are the dependencies, and the selected target (in blue) is the one that will be combining both:

image

If you check the build phases, you will see that the 2 static libraries are listed as Target Dependencies. Xcode build will include Pods-RavelAntunesApp-AFNetworking and Pods-RavelAntunesApp-SVProgressHUD into Pods-RavelAntunesApp when building.

image

If you go back to your project, and select your main target, you will see that Pods-RavelAntunesApp now is a Target Depedency of that target.

image

The diagram below might make it easier for you to understand:

image

The same happens to the test target. Your test target will include a Pods static library, which will include your dependencies’ static libraries.

Also, looking at the “Link Binary With Libraries” in your target, you will notice the absence of other frameworks that would normally need to be included in your project. You don’t have to, as they are already included in the static libraries compiled by CocoaPods.

How Does CocoaPods Know How to Install Those Dependencies?

CocoaPods dependencies usually resolve from a source control repository, in most cases Git. To be resolved by CocoaPods, the dependency must include a NAME-HERE.podspec. For example, here is AFNetworking.podspec. Looking at the file, you can actually see that AFNetworking is composed of many other dependencies, outlined by each s.subspec. Each subspec also has its specific settings, like which frameworks it requires.

This gives you some flexibility. If you didn’t want to include AFNetworking with all dependencies, you could just list the ones you want. (There’s a few discussions where people are requesting an exclude feature, so you could simply specify some subspec to exclude, instead of listing all that you want to include).

pod 'AFNetworking/NSURLSession'
pod 'AFNetworking/NSURLConnection'

You end up with a leaner project.

Another good under-the-hood perspective of CocoaPods can be read here.

Benefit #2 - Creating Your Own Dependencies

There are 2 main reasons why you might want to create your own dependencies (which are almost the same thing, but with different contexts):

  1. The most obvious one: you have this nice piece of code you came up with, and you want to share with the open source community.
  2. You need a piece of functionality to be reused within an organization without duplicating code.

I’m gonna focus on an example for the second one, as the first seems pretty straightforward. I’ve worked in a few different organizations where they might have different apps and projects, but they make use of the same core pieces of functionality. I’m gonna use the Facebook app as an example(I’m not sure how their development process is, this is just for explanation). Facebook has a few different apps currently in the App Store. They have their main Facebook app for their social media, but they also have the Facebook Messenger app, dedicated for their chatting system. They have 2 different functionalities, but they probably share a lot of common code.

For example: - Fetch user information. - Fetch list of friends. - Fetch list of groups. - Until recently, the Facebook app had the messenger capability in it.

We can modularize those functionalities in a few different pieces. For example, we could have a service layer library with all our network calls, so all the apps that interact with the API can just use that same library. We could also keep chat core functionality separated on its own, so we can include it on Facebook App and on Messenger. Since chat needs to interact with the API, it should depend on the service layer library we created. Last, let’s have our Facebook App and Messenger depend on the service layer for the network calls, but also on the chat library.

If we were to diagram the module dependencies, we would end up with something like that:

image

The biggest advantage of that is keeping everyone in sync. Let’s say the team responsible for the service layer library discovers a critical bug in its code, and fixes it. If we were copying and pasting code around, we would have to do that in 3 different projects. If we were using dependency management, they could simply push their code to their repository as a new version, and any project depending on it could resolve the new version.

If in this case we were using CocoaPods, we would have created different pods for the service layer and the chat. Each one we would add a .podspec file on the repository so CocoaPods knows how to include it in your project. Also, when adding them as dependency, we can just not specify the new version number, so every time the project dependencies are resolved, it will get the latest version. I like to do that when working with internal dependencies, specially if I have a continuous integration server running. If that new dependency change breaks anything within my app, I would know right away.

Benefit #3 - Integrate New Library Versions

If you are like most developers I know, you want to always have the latest libraries and framework versions. It’s not only good to have newer versions so you can take advantage of new features, but most likely those new versions will include bug fixes or performance improvements to older versions.

Similar to what I mentioned on the last paragraph of the last item, you can be integrating your app with the new version of libraries every time you resolve your dependencies, or run pod install. Now, it can be a little dangerous to keep it like that, specially if you don’t have a continuous integration server or a good set of automated tests. A new version of a library can easily break your app silently, so it can be dangerous to leave it without an explicit version. Most dependency management tools give you a way of not specifying the version.

An approach you can take to integrate with new versions without risking your main target is to duplicate your target and build scheme. In your Podfile file, make a specific block of dependencies by duplicating the dependencies of the first target, but without including the version numbers. If you are running a CI server, have your server execute a separate task for the the new build scheme building that new target. That way, if the CI build or test breaks your duplicated target without breaking your main target, most likely it was due to a dependency update.

See my first blog of this series about build schemes and targets for more info.

Benefit #4 - Keeping Licenses Together (CocoaPods Specific)

An often overlooked process and almost a hidden feature, CocoaPods helps you manage the licenses of your dependencies. Once you run a pod install, it will generate a Pods--acknowledgment.plist per target with all dependencies included. A lot of developers don't pay much attention to that, so that's a nice feature for when you or management want to easily audit if your project is not infringing any copyright, and save you from some legal trouble.