CocoaPods Explained: Podfile

In iOS development, CocoaPods has become one of the must-know tools. In a team where CocoaPods is adopted, engineers run pod install as often as git checkout. They know how to properly set up a project with CocoaPods, declare dependencies in Podfile, and update dependencies if needed. However, some have little understanding of what happens under the hood. This prevents them from troubleshooting CocoaPods issues if occurred or extending its usage. The “CocoaPods Explained” series I am writing aims to fill that gap.

In this post, we take a look at Podfile and try to understand it a little bit more.

Declaration in Podfile

Podfile is where we declare dependencies of the project. This file is just a snippet of ruby code on top of provided methods (reference: Podfile syntax).

platform :ios, "14.0"
use_frameworks!

target "App" do
  pod "RxSwift"
  # ...
end

In Podfile, we declare a dependency using the pod function. Such a dependency could be from CocoaPods-managed repos, a git repo, or a local directory:

pod "RxSwift", "6.1.0"
pod "Moya", :git => "https://github.com/Moya/Moya.git", :tag => "15.0.3"
pod "Services", :path => "LocalPods/Services"

When is Podfile loaded?

Not all pod commands load this Podfile. We can easily verify this hypothesis by printing a log message at the beginning of Podfile (ex. prepending this line puts "Load Podfile"). The message appears in the logs when running pod install while it does not when running some other commands like pod repo list.

This behavior makes sense because Podfile is meant for dependencies declaration. CLI usages related to dependencies should load Podfile. Otherwise, loading Podfile is not necessary. Looking at the pod subcommand descriptions (ie. running pod --help), one may intuitively tell that pod install, pod update, and pod outdated do load Podfile.

cli_pod_help.png

Looking at the code of cocoapods and cocoapods-core, Podfile is loaded when we first access Pod::Config.instance.podfile (ie. the attribute podfile of the config singleton).

https://github.com/CocoaPods/CocoaPods/blob/1.11.0/lib/cocoapods/config.rb#L204-L206

module Pod
  class Config
    def podfile
	    @podfile ||= Podfile.from_file(podfile_path) if podfile_path
    end
    ...
 end
end

We can verify this easily by executing a one-line ruby code that accesses the value. The log Load Podfile should appear in the console.

$ ruby -e 'require "cocoapods"; Pod::Config.instance.podfile'

Load Podfile

Methods in Podfile

Podfile content is evaluated within Pod::Podfile context. This gives us code access to Pod::Podfile’s instance variables and private methods. Therefore, what we can do in Podfile is much more than just dependencies declaration.

Have you ever got annoyed by the Podfile checksum changing all the time? This change causes a high chance of merge conflicts in Podfile.lock, especially when there are many contributors to the project.

-PODFILE CHECKSUM: ad5874f24bb0dbffd1e9bb8443604e3578796c7a
+PODFILE CHECKSUM: ab71b2c6800abc9eba8a3500b110eccf4e33e7f4

We can tackle this problem by setting the @checksum attribute right in Podfile. After running pod install, we should see the dummy checksum being reflected in Podfile.lock.

# In Podfile
@checksum = "dummy-text-to-reduce-merge-conflicts"

As mentioned, we have access to Pod::Podfile’s methods in Podfile. Those commonly used in Podfile are use_frameworks!, platform :ios, "14.0", target "...", pod "...". In fact, those methods are defined in Pod::Podfile::DSL and included in Pod::Podfile.

We can patch those methods to add more customizations. This could be done using alias_method with the corresponding method in Pod::Podfile or Pod::Podfile::DSL. Using alias_method is similar to the method swizzling technique in Swift. Some CocoaPods plugins alter CocoaPods’s behaviors this way to interfere with the pod installation process.

The following code demonstrates how we introduce the :xcode_migration option to the pod syntax. This option helps eliminate many if/else checks in Podfile, making the usage more declarative.

# In Podfile
require_relative "patch" # <-- Load patch.rb
...
pod "RxSwift", :xcode_migration => {
  :default => "6.1.0",
  :xcode14 => {
    :git => "link/to/fork",
    :branch => "xcode14"
  }
}
# In patch.rb
module Pod
  class Podfile
	  alias_method :original_pod, :pod
	  def pod(*args, **kwargs)
      if (opts = kwargs[:xcode_migration])
        # Process opts & update args/kwargs to use the version/branch accordingly
        ...
        if xcode14?
          ...
        end
			end
	    original_pod(*args, **kwargs) # <-- call the original method with the processed args/kwargs
		end
  end
end

Above are some explanations of how Podfile works. More topics will be covered in the upcoming posts. Stay tuned!