Modular Objective-C
Modular Objective-C has been a good conversation starter lately, yet it’s still a little bit of an unexplored topic. Modular can mean lots of things when seen from different points of view, but this post will focus primarily on organizing code. I had some interesting challenges to solve at Yammer: In the past year we’ve completely reorganized our iOS codebase, shipped brand new iPad and iPhone apps, while getting our code in a way better shape. This allowed us to move faster and have weekly iOS releases.
CocoaPods
A tool that has been around for a while, originally written by Eloy Durán and Fabio Pelosin, has brought some new ideas of tackling code organization; it has encouraged people to properly do Semantic Versioning, tag code, and think how to write better, reusable software. This post is heavily based on CocoaPods, but you can apply these principles to other similar systems.
What can we share
A well written mobile app may consist of a few segments or concerns, like:
- Application framework (
UIKit,AppKit) - Application framework extensions (e.g. custom animations)
- Networking layer
- Persistence layer
- Configuration layer (treatments, localization, …)
- Domain (business) logic
If you take a look on the bullet points above as they’re separated, avoid
coupling these! Don’t couple persistence with UIKit or AppKit. Don’t
couple business logic with persistence; don’t couple networking with
persistence.
Given the above example, let’s try to build a sane project structure that can be reused and shared among developers on the team. We’ll focus on iterating as fast as possible, and because of that we won’t use an intermediate podspec server.
Project structure
$ tree -d
.
├── Silicon
│ ├── Silicon.xcodeproj
│ └── Silicon.xcworkspace
└── SiliconKit
├── Source
└── Specs
Example folder structure for our project Silicon. It’s an incredible app for finding investors, evaluating companies on the fly, getting funded, and so on. Except it doesn’t exist in the real life.
While building this disruptive piece of technology that’s about to change
the way people work, think and create, it was a good idea to separate
shareable files into SiliconKit. We can use it build an iPad app or even
better, find a business model and rent the framework as SAAS.
$ tree SiliconKit
SiliconKit
├── Makefile
├── Resources
├── SiliconKit.podspec
├── SiliconKitTests.podspec
├── Source
│ ├── Data
│ ├── Investors
│ ├── Networking
│ │ ├── Authentication
│ │ ├── Companies
│ │ ├── Investitions
│ └── Presenters
└── Specs
├── Data
├── Investors
├── Networking
│ ├── Authentication
│ ├── Companies
│ ├── Investitions
└── Presenters
One important thing is that Source and Specs folders consist only of
plain-old Objective-C files. There’s no .xcodeproj or any build related
stuff. This way we make it trivial to maintain and move around without breaking
the whole build system. I mentioned not using an intermediate podspec server;
but we somehow need to keep track of the SiliconKit state. You need to be able
to go back in time, and for any commit tell - this is the state where it
worked. One way of resolving this is to use git submodules for having the SHA
committed, but I’ll let your creativity solve this one.
Podspecs
Podspecs are here to tell Xcode how to organize and link files with certain
targets. You can create and rename files using any editor or even Finder.app;
pod install will take care of linking them properly. We’ll make two podspecs,
one for production files and one for testing. We need a separate testing one so
we can decouple test frameworks and settings from the build system.
Production:
Pod::Spec.new do |s|
s.name = 'SiliconKit'
s.source = { :git => 'https://silicon.github.com/silicon/siliconkit.git' }
s.subspec 'Investors' do |ss|
ss.source_files = 'SiliconKit/Investors/**/*.{h,m}'
ss.resources = 'SiliconKit/Investors/**/*.{xib,png,lproj,bundle}'
ss.dependency 'ObjectiveSugar', '~> 1'
ss.prefix_header_file = 'SiliconKit/Investors/Investors-Prefix.pch'
end
s.subspec 'Investors' do |ss|
# ... ommitted ...
end
s.subspec 'Data' do |ss|
# ... ommitted ...
end
s.subspec 'Networking' do |ss|
# ... ommitted ...
end
# ... some flags ommitted ...
end
Test:
Pod::Spec.new do |s|
s.name = 'SiliconKitTests'
s.source = { :git => 'https://silicon.github.com/silicon/siliconkit.git' }
s.source_files = 'Specs/**/*.{h,m}'
s.xcconfig = {
'GCC_WARN_UNDECLARED_SELECTOR' => 'NO',
'GCC_GENERATE_TEST_COVERAGE_FILES' => 'YES',
'GCC_INSTRUMENT_PROGRAM_FLOW_ARCS' => 'YES'
}
# ... some flags ommitted ...
s.frameworks = 'XCTest'
s.dependency 'SiliconKit'
s.dependency 'Kiwi/XCTest' # Notice how podspec takes care of Kiwi dependency
end
In order to build the Silicon.app, this is how your Podfile would look
like:
platform :ios, '7.0'
pod 'SiliconKit', :path => '../'
target :SiliconTests, :exclusive => true do
pod 'SiliconKitTests', :path => '../' # no need to import Kiwi
end
Now, you might be asking – why in the world are here 2 podspecs? This enables us to do something really cool: press CMD + U, and run both our app’s tests (Silicon tests) together with SiliconKit’s tests. If you have more apps with decoupled test targets, you can make a compound test suite as easy as:
platform :ios, '7.0'
pod 'SiliconKit', :path => '../'
target :SiliconTests, :exclusive => true do
pod 'SiliconKitTests', :path => '../'
pod 'MenloParkTests', :path => '../'
pod 'MountainViewTests', :path => '../'
end
Of course - you won’t run these every time while developing, but makes sense to
do it on an CI server when you want to test against many SDKs / devices. The
most common point of failure is while developing a concrete app together
with SiliconKit, one might change an API in shared code itself. While all
the tests are passing in context of that particular app, some other
dependants might start failing. For a more visual explanation of the
problem, take a look at the illustration below:
Wrapping up
Your shared code should consist only of source files and domain resources.
Having extra Xcode projects will bite you in the long run; let CocoaPods do
project organization and target linking for you. Decoupling your domain logic
from frameworks like UIKit, AppKit, CoreData, will let you reuse it in
any of your projects. I’ll try to write on this subject later.