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.
While working on Yammer, I’ve seen some interesting challenges to solve; In the past year we’ve completely reorganized the iOS code, shipped brand new iPad and iPhone apps, and got our codebase in a way better shape. This also allowed us to move faster and do 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 also encouraged people to properly do Semantic Versioning, tag code, and think how to write better reusable software.
In this post I’ll assume you’re familiar with CocoaPods.
What can we share
A well written mobile app may consist of a few segments or concerns, like:
- Application framework (
UIKit
,AppKit
) - Your own application framework extensions (custom controllers, animations)
- Networking layer (these files where you probably #import AFNetworking)
- Persistence layer
- Configuration layer (treatments, localization, …)
- Domain (business) logic
If you take a look on the bullet points above as they’re separated, it might
ring a bell: avoid coupling these!. In the real world, 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 the intermediate podspec server.
Project structure
$ tree -d
.
├── Silicon
│ ├── Silicon.xcodeproj
│ └── Silicon.xcworkspace
└── SiliconKit
├── Source
└── Specs
I’ve made a small example structure for 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’ve mentioned not using an intermediate podspec server; but 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.
You may want to use git submodule
only for having 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
This means, in order to be a Silicon.app developer, 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 an 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
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.
Codewise - 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.