You probably have looked at CocoaPods by now and found it to be a great way to quickly pull together all your favorite Open Source components for an app. After our recent move to Git I researched some more and found that it is exceptionally easy to also provide specs for your closed source private code.
Update: Updated Resource Bundle generation use the new syntax of CocoaPods 0.18.
CocoaPods has a number of repositories that it derives its specs from, for a default install this number is 1. But it doesn’t have to be, any other git repository can also serve spec files, as long as it obeys the standard layout. You have a folder by component and inside this you have one folder per version which contains a lonely podspec file.
Resource Bundles and Pods
Before I could get started in manufacturing spec files for my components I needed first to solve a problem. If a component requires some resources I always put them in a resource bundle target. I hate it when Open Source components have a static bundle as a workaround for CocoaPods approach of copying files. Those don’t get the proper processing they deserve, images get processed, strings files get converted into binary plists and more.
Fortunately other people are thinking the same and so I quickly got an answer on the CocoaPods Google Group, Fabio Pelosin came to my rescue and pointed me towards the spec of CoconutKit 2.0.2 which also builds a resource bundle, written by none other than famous Cédric Luthi aka @0xced. In the company of Giants…
Cedric has a bit weird structure in my opinion having a separate xcodeproj just for the resources, so my approach is a variation and simplification of his.
First you need to tell CocoaPods to preserve the xcodeproj and the resource folders because without that it deletes these and only keeps the source code files.
Then you need to build the resource bundle into the common Resources folder and add the file to the resource install script. Here is the complete spec for DTLoupe:
Pod::Spec.new do |spec| spec.name = 'DTLoupe' spec.version = '1.3.0' spec.platform = :ios, '4.3' spec.license = 'COMMERCIAL' spec.source = { :git => 'git@git.cocoanetics.com:parts/dtloupe.git', :tag => spec.version.to_s } spec.source_files = 'Core/Source/*.{h,m}' spec.frameworks = 'QuartzCore' spec.requires_arc = true spec.homepage = 'http://www.cocoanetics.com/parts/dtloupeview/' spec.summary = 'A Loupe as used for text selection.' spec.author = { 'Oliver Drobnik' => 'oliver@cocoanetics.com' } spec.preserve_paths = 'DTLoupe.xcodeproj', 'Core/Resources' def spec.post_install(target_installer) puts "\nGenerating DTLoupe resources bundle\n".yellow if config.verbose? Dir.chdir File.join(config.project_pods_root, 'DTLoupe') do command = "xcodebuild -project DTLoupe.xcodeproj -target 'Resource Bundle' CONFIGURATION_BUILD_DIR=../Resources" command << " 2>&1 > /dev/null" unless config.verbose? unless system(command) raise ::Pod::Informative, "Failed to generate DTLoupe resources bundle" end end if Version.new(Pod::VERSION) >= Version.new('0.16.999') script_path = target_installer.target_definition.copy_resources_script_name else script_path = File.join(config.project_pods_root, target_installer.target_definition.copy_resources_script_name) end File.open(script_path, 'a') do |file| file.puts "install_resource 'Resources/DTLoupe.bundle'" end end end |
Note the single quotes which are necessary for the target since I have a space in there.
I later found that I had version 0.16 of CocoaPods on the machine where I put together the spec. People using the latest version (presently 0.18.1) would get quite a few warnings on pod install and update. 3 problems to be exact.
The first problem was that I thought I was smart by removing the preserved folders after the install. Turns out that these are still needed even for an update.
The second problem came from target_installer.target_definition.copy_resources_script_name being deprecated, instead some people (e.g. HockeyKit) switched to target_installer.copy_resources_script_path instead.
However – problem number three – was that access to the global config singleton was deprecated as well. So each access to it causes a nasty yellow warning to appear. Eloy Durán explained to me that it was a “chicken and egg thing”. He also – wisely – suggested that I should totally drop support for older CocoaPods versions.
Bonus problem number four is that as of 0.18 there is a built in post_install hook and so there’s an occasional warning the the def of it.
People get a warning if they have an old CocoaPods version if they do anything and updating is simple with a sudo gem update cocoapods. I am a Ruby NOOB so I am thankful for Eloy helping to rewrite the spec as follows. I proceeded to removed all references to the config singleton.
Pod::Spec.new do |spec| spec.name = 'DTLoupe' spec.version = '1.3.0' spec.platform = :ios, '4.3' spec.license = 'COMMERCIAL' spec.source = { :git => 'git@git.cocoanetics.com:parts/dtloupe.git', :tag => spec.version.to_s } spec.source_files = 'Core/Source/*.{h,m}' spec.frameworks = 'QuartzCore' spec.requires_arc = true spec.homepage = 'http://www.cocoanetics.com/parts/dtloupeview/' spec.summary = 'A Loupe as used for text selection.' spec.author = { 'Oliver Drobnik' => 'oliver@cocoanetics.com' } spec.preserve_paths = 'DTLoupe.xcodeproj', 'Core/Resources' spec.post_install do |library_representation| Dir.chdir File.join(library_representation.sandbox_dir, 'DTLoupe') do command = "xcodebuild -project DTLoupe.xcodeproj -target 'Resource Bundle' CONFIGURATION_BUILD_DIR=../Resources" command << " 2>&1 > /dev/null" unless system(command) raise ::Pod::Informative, "Failed to generate DTLoupe resources bundle" end end File.open(library_representation.copy_resources_script_path, 'a') do |file| file.puts "install_resource 'Resources/DTLoupe.bundle'" end end end |
In summary: use the second approach if you don’t want warnings and don’t need to support older CocoaPods versions.
Spec’ing it Out
Because DTRichTextEditor and DTLoupe are closed source that I am selling access too it would not have made any sense to put their specs into the public CocoaPods. Also it would probably fail the CI checks they have for lint-ing the specs because their Continuous Integration server would not be able to clone the source.
So I added a specs repository on our private GitLab server and made it public. The next question was if you would reference components with the SSH or HTTPS URLs. I went with SSH because there authentication works nicely by the public SSH keys the users have added to their profiles. If you use HTTP then users have to enter their credentials.
The security of SSH also makes me not worry security for my components and which is why I am showing you the above spec. Please be nice, don’t hack me bro…
Before I could put together the DTRichTextEditor Spec I needed to know the minimum versions that all dependencies would need to have to be compatible. For this I tagged and published specs for DTFoundation 1.2 and DTCoreText 1.4.1. The final step was to change around some headers in the project so that it would pass pod spec lint.
CocoaPods ignores any headers you added to your PCH file, because at the time of building the pods there IS no Precompiled Header File. At least not the one you were using for building your static library. Running the spec lint with –no-clean proved invaluable because it quickly found the places where additional #import where necessary.
I made this graphic to show how all fits together. DTLoupe and DTRichTextEditor are on our private GitLab server. They references DTWebArchive, DTCoreText and DTFoundation as dependencies.
Version Confusion aka The Pessimistic Operator
One thing that took me a while to wrap my head around was how to properly write the version requirement. At first I was pointed towards the CocoaPods Dependency Versioning document but that didn’t help clarify. French Developer Arnaud helped me out there.
~> 1.1.7 is >= 1.1.7 && < 1.2 And ~> 1.1 is >= 1.1 && < 2.0
What confused me is that the individual digits of the version are meaning something else. The rightmost digit means equal than or higher whereas all other digits are “locked”. This process is explained in the RubyGems User Guide. They call the tilde-greater the “pessimistic version constraint”.
If you are unsure how to increment your version numbers then I recommend you read the Semantic Versioning Specification authored by Tom Preston-Werner, one of the GitHub co-founders.
Conclusion
From my own experience I can tell you that it is a great feeling to have your dear-to-your-heart component successfully build via CocoaPods because it means that you don’t have a hidden dependency that you didn’t know about.
I will now make it standard practice to spec out private and public components because the process of getting it to pass lint alone shows you some places where you might have some build issues.
And the benefit for my customers is obvious as well. They can now have 3 options to add DTRichTextEditor to their apps:
- CocoaPods
Of these I personally use the second most of the time since it allows me to add code to sub-modules while working on a component. But for future apps I am tempted to switch everything to using CocoaPods.
Categories: Updates
Great article. I like the idea of using CocoaPods to manage dependencies everywhere. With git submodules the dependecy can change anytime without warning, because it is editable anywhere it is used (of course if you have write permission).