https://mar.in | Github | Mastodon | X

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:

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:

Changing SiliconKit together with MenloPark breaks PaloAlto and Silicon
Changing SiliconKit together with MenloPark breaks PaloAlto and Silicon

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.