Modular Objective-C

18 Apr 2014

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
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.

BTW, sorry for a little bit of trolling and never forget: #startuplife