Ad

Our DNA is written in Swift
Jump

Digging into CocoaPods

There once was a developer who figured that it would make sense to not reinvent the wheel, or at least not all 4 that he needed for his app/car. He had previously learned how to contribute to open source projects on github and wrapped his head around git submodules. As long as you stay in the git ecosystem all is bliss, submodules contain Xcode projects which are easily added as sub-projects.

The big advantage of sub-projects is that you can debug into these and if you fix something you can easily push that back to the master repository. But this convenience brings with it a drawback: since you have to keep a copy of each sub-module in each project structure that needs them you risk ending up with many different versions of many different components all over your file system.

Now add to this a second level of complexity: what about components that themselves have further sub-components, probably contained in other github repositories? Before CocoaPods there was no good way to managed these sorts of multi-level dependencies.

The idea behind CocoaPods is to describe a heap of code belonging together in a podspec file. Add information on which external frameworks and libraries this component requires and sprinkle over a bit of meta information, like in which GitHub repository a certain version of the code can be found. Then put this spec file into a global repository of spec files that can be searched.

There are great tutorials on how to use other people’s pods in your apps. Let’s just briefly boil it down to the bare bone steps.

Using Pods

Here are the absolute minimum steps:

  1. Install CocoaPods and update your ruby gems as described in the tutorial.
  2. Find some pods to use with pod search
  3. Create a new Xcode project
  4. In the project root folder create a Podfile describing your platform and deployment target plus the needed pods
  5. pod install
  6. You now have an Xcode workspace file with everything set up

The resulting workspace has two Xcode projects, one being the one you created in step 3, the other being all the source code from all the referenced pods.

Workspace with Pods

 

All the pod source code is compiled into a static library that your own project has as a dependency. So it will be built before your project and the static library will be linked to your code. The above example has this Podfile:

platform :ios, '5.0'
pod 'DTFoundation/Core'
pod 'DTDownload'

This specifies that we are not supporting less than iOS 5, we need any version of DTDownload and the Core subspec of DTFoundation.

Behind the scenes CocoaPods refreshed the local copy of the Specs repository, found DTFoundation and DTDownload, cloned both these git repositories into a caches folder, assembled a Pods.xcodeproj and tied it all together with our test2.xcodeproj.

To use any function contained in any of the pods you only need to import the appropriate header and then you use it. For example here I am using the cached cachesPath from NSString+DTPaths.

Using Something

 

Awesome stuff! That’s all we’ll look at for a tutorial because what we are really interested now is how this looks from the point of view of the component vendor.

Ins and Outs of Pod Spec Creation

I was forced to learn about CocoaPods subspecs and spec syntax when somebody requested an updated podspec for DTCoreText. In the past this would contain copies of DTVersion and DTHTMLParser amongst the regular source code of this library. Then at some point I decided that people can be taught how to use git submodules and so these items were removed and instead DTFoundation added as a sub-project.

DTFoundation didn’t have its own pod spec mostly because I was not using CocoaPods until now. I had started with DTFoundation as a central repository of all my reusable code. Some parts of it where requiring iOS 5: DTBonjour because it uses zeroing weak refs, DTDownloadCache because it needs external file references. So I set up DTFoundation’s podspec such that all the individual items requiring extra frameworks to be linked are their own sub-specs.

Turns out that there is problem with subspecs requiring a higher OS version than the spec itself. This is why I finally caved and moved DTDownload and DTBonjour into their own git repositories plus dedicated spec files as opposed to having them as sub-specs of DTFoundation.

Let me show you my specs:

DTFoundation.podspec

Pod::Spec.new do |spec|
  spec.name         = 'DTFoundation'
  spec.version      = '1.0.0'
  spec.summary      = "Standard toolset classes and categories."
  spec.homepage     = "https://github.com/Cocoanetics/DTFoundation"
  spec.author       = { "Oliver Drobnik" => "oliver@drobnik.com" }
  spec.source       = { :git => "https://github.com/Cocoanetics/DTFoundation.git", :tag => spec.version.to_s  }
  spec.license      = 'BSD'
  spec.requires_arc = true

  spec.subspec 'Core' do |ss|
    ss.ios.deployment_target = '4.3'
    ss.ios.source_files = 'Core/Source/*.{h,m}', 'Core/Source/iOS/*.{h,m}'
    ss.osx.source_files = 'Core/Source/*.{h,m}', 'Core/Source/OSX/*.{h,m}'
  end

  spec.subspec 'DTHMLParser' do |ss|
    ss.ios.deployment_target = '4.3'
    ss.dependency 'DTFoundation/Core'
    ss.source_files = 'Core/Source/DTHTMLParser/*.{h,m}'
    ss.library = 'xml2'
    ss.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2' }
  end

  spec.subspec 'DTZipArchive' do |ss|
    ss.ios.deployment_target = '4.3'
    ss.source_files = 'Core/Source/DTZipArchive/*.{h,m}'

    # Ideally minizip should have a Pod
    # ss.dependency 'Minizip'
    ss.subspec 'Minizip' do |sss|
      sss.source_files = 'Core/Source/Externals/minizip/*.{h,c}'
    end
  end

  spec.subspec 'DTUTI' do |ss|
    ss.ios.deployment_target = '4.3'
    ss.ios.frameworks = ['MobileCoreServices']
    ss.source_files = 'Core/Source/DTUTI/*.{h,m}'
  end
end

DTCoreText.podspec

Pod::Spec.new do |spec|
  spec.name         = 'DTCoreText'
  spec.version      = '1.1.0'
  spec.platform     = :ios, '5.0'
  spec.license      = 'BSD'
  spec.source       = { :git => 'https://github.com/Cocoanetics/DTCoreText.git', :tag => spec.version.to_s }
  spec.source_files = 'Core/Source/*.{h,m,c}'
  spec.dependency 'DTFoundation/Core', '~>1.0'
  spec.dependency 'DTFoundation/DTHMLParser', '~>1.0'
  spec.frameworks   = 'MediaPlayer', 'QuartzCore', 'CoreText', 'CoreGraphics', 'ImageIO'
  spec.requires_arc = true
  spec.homepage     = 'https://github.com/Cocoanetics/DTCoreText'
  spec.summary      = 'Methods to allow using HTML code with CoreText.'
  spec.author       = { 'Oliver Drobnik' => 'oliver@drobnik.com' }
  spec.library      = 'xml2'
  spec.xcconfig     = { 'HEADER_SEARCH_PATHS' => '"$(SDKROOT)/usr/include/libxml2"' }
  spec.prefix_header_contents = '#import <CoreText/CoreText.h>'
  spec.resources = 'Demo/Resources/DTCoreTextFontOverrides.plist'
  def spec.post_install(target)
    Dir.chdir(config.project_pods_root + 'DTCoreText/Core/Source/') do
      Dir.glob('*.css') do |css_file|
        system '/usr/bin/xxd', '-i', css_file, css_file + '.c'
      end
    end
  end
end

It took me a great deal of support from the great developers working on CocoaPods and a few others to end up with these. So you better appreciate their beauty and shut up. 😉

Some things where not immediately obvious, so I’ll document them here.

  1. You cannot (with the current CocoaPods version) have a sub spec with a higher deployment target than the main spec. Conversely the dependency resolver always checks the main spec deployment target even if all you want is to get at a subspec.
  2. Reuse the spec.version as the source tag – spec.version.to_s converts it to a string.
  3. If you have differing source files for iOS and Mac you have to make one dedicated spec.{ios|osx}.source_files line for each.
  4. You can specify dependencies to other specs or sub specs including a minimum version
  5. You can specify resources that need to be copied from your pod into the target app. Those are not to be found in the Copy Bundle Resources build phase as you might expect, but there is a custom shell script “Copy Pods Resources” taking care of that.Resource are copied
  6. If you need some file processing to occur post install, you can def spec.post_install(target)
  7. If you need to set extra Xcode config parameters then there’s spec.xcconfig for that.
  8. The spec.prefix_header_contents adds its contents NOT to the app’s PCH, but to Pods-prefix.pch.
  9. pod spec lint mylib.podspec –verbose –no-clean is your friend, it leaves the test xcodeproj at /tmp/CocoaPods/Lint/Pods/Pods.xcodeproj/ where you can see what CocoaPods sees.
  10. You need to tag the version you are creating the podspec for. If you find that you need to re-tag because you had to fix something, then you have to remove the CocoaPods cache so that pod spec lint gets the newer version. rm -rf ~/Library/Caches/CocoaPods/Git*
  11. All paths for source files and resources are relative to the project root and cannot begin with a slash
  12. You have to make sure that in the Specs repository you have a folder for your project, then a folder for each version and the podspec file goes inside that. If you accidentially put it next to the version folder, then you get an an error email from Travis-CI (Continuous Integration) server.
  13. If you require special frameworks, for example QuartzCore then you won’t find these in the frameworks folder. Rather CocoaPods adds these to the Other Linker Flags

Pods are really awesome once you have your spec files correct. And if you know which pods you want to use you can get your app started in under a minute. There is a slight disadvantage for the makers of the pods you use: if you find a bug in the source code that CocoaPods copied into the work space you cannot just go in, fix it and push it upstream. You have to do these modifications in a clone of the original repo.

Conclusion

The inability of pushing changes upstream is a small price to pay for the convenience of having your web of dependencies being detangled for you. And this drawback only really exists for the people who maintain these open source components. As a consumer of free code you can usually rely on stable versions being codified as spec files and added to the global Specs repo.

It is too early yet for me to say what effect this knowledge will have on my development future. If anything then I’ll probably embrace Xcode workspaces. Or maybe something pushes me over the edge and I’ll convert all my apps to pods.

But if anything then I will add a podspec to all my open source components in the future, now that we know how its done, even with sub-sub-components.


Categories: Recipes

11 Comments »