Cocoapod as XCFramework With Dependencies

Written by maxkalik | Published 2023/02/17
Tech Story Tags: mobiledebugging | xcframework | framework | cocoapods | xcode | pods | software-development | hackernoon-writing-contest

TLDRIf you want to include a dependency from a specific source in your cocoapod XCFramework you need to literally include this binary in a final zip of your XCFramework.via the TL;DR App

This article will be useful for those who struggle to build an iOS framework as a service for customers. I know there is still a lack of information on the internet and Apple has not been so obvious about how to create and use iOS Frameworks, especially if this framework should have dependencies. Maybe because Apple itself doesn’t recommend using dependencies in frameworks, or maybe it’s just a pretty rare case that we are going to consider here.

If you are a rockstar expert and this question for you is a trivial one, please add some comments to this article, maybe you have additional information that we should know — I really appreciate it, at least here we could gather useful guides for everyone who suffers (like me some time ago) to build and maintain a framework with dependencies and ship it as a cocoapod. Let’s go.

XCFramework

Let’s define our goal more concretely. Imagine a story where you are an iOS Engineer and you have a client. This client came to you and asked to create an iOS framework that should do almost everything (imagine something like a complex solution, e.g. online bank or you name it). The source code, of course, should be closed and only some public methods should be exposed outside and everything work in simulators and real devices. At this point you would say that our framework will be XCFramework and you are right.

For those who didn’t know what is this, take a look at a note from Apple’s documentation:

An XCFramework bundle, or artifact, is a binary package created by Xcode that includes the frameworks and libraries necessary to build for multiple platforms (iOS, macOS, tvOS, and watchOS), including Simulator builds. The frameworks can be static or dynamic and also include headers.

So, our final product will be an XCFramework, and it should be installed as a cocoapod.

But as always there is nuance: this product needs to be done ASAP. You as a developer of course can write everything by yourself, but how long you can do this? You don’t have much time. Of course, you need to find ready-to-use solutions. This means our framework will have some dependencies.

Your cocoapod as XCFramework

Before diving into the process let’s prepare a list of how we are going to test our cocoapod XCFramework.

  1. With open-source dependencies
  2. Your XCFramework > 100MB
  3. XCFramework with XCFrameworks dependencies
  4. With dependencies that use XCFrameworks inside
  5. XCFramework with dependency as XCFrameworks from special source path

Let's go through all of these points and test all these cases together with me describing in detail the progress.

1. With open-source dependencies

Let’s start with something simple and inject some open-source dependencies into our project. If you created your framework project and did pod init then you will have this Podfile. Open it and let’s add some dependencies.

platform :ios, '14.0'

target 'YourFramework' do
  use_frameworks!

  pod 'CropViewController'
  pod 'Kingfisher'

end

As you can see I didn’t add some stupid abstract fake cocoapods because this experiment would be just useless. To be more realistic I added existing (randomly picked) cocoapods that will help you to debug by yourself.

Also, I’m going to give you some tips for avoiding some problems in the future. So catch the first one:

Tip: Don’t forget to lock the versions of all dependencies.

platform :ios, '14.0'

target 'YourFramework' do
  use_frameworks!

  pod 'CropViewController', '2.6.1'
  pod 'Kingfisher', '7.5.0'

end

Locking versions will safeguard you from unexpected crashes on the client side because the clients can use the same dependencies. Even the smallest differences between versions can lead to the fails.

Now, it’s time to build our XCFramework and for that, we will use 3 scripts:

  1. Archiving for simulators
  2. Archiving for iPhones
  3. Combining two artifacts into the single bundle XCFramework

# 1
xcodebuild archive \
-workspace MyFramework.xcworkspace \
-scheme MyFramework \
-configuration Release \
-sdk iphoneos \
-archivePath archives/ios_devices.xcarchive \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
SKIP_INSTALL=NO \

# 2
xcodebuild archive \
-workspace MyFramework.xcworkspace \
-scheme MyFramework \
-configuration Debug \
-sdk iphonesimulator \
-archivePath archives/ios_simulators.xcarchive \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
SKIP_INSTALL=NO \

# 3
xcodebuild \
-create-xcframework \
-framework archives/ios_devices.xcarchive/Products/Library/Frameworks/MyFramework.framework \
-framework archives/ios_simulators.xcarchive/Products/Library/Frameworks/MyFramework.framework \
-output MyFramework.xcframework

In my case, this script I called build.sh and saved it in the project. You can run this script to get your fresh-baked XCFramework, which we are going to distribute.

2. Your XCFramework > 100MB

Let’s complicate the task a little bit (we are not the ones who take the easy way). Our small complication lies in its size that more than 100MB. It means the heavy framework we cannot just upload in GitHub. Instead of using GitHub repository, we need to upload our framework somewhere. It can be any public storage. You can use Github Large Files Storage or (like me) Google Cloud Storage.

So, before configuring podspec instructions zip your XCFramework and upload it to the storage.

Tip: Rename your framework zip file using the format: <Framework_Name><Version>.zip

Having a version in the zip filename of your framework is extremely useful because it will allow your customers to install a particular version of your framework.

To distribute the framework as cocoapod we need to prepare podspec file:

Pod::Spec.new do |s|
  s.name          = 'MyFramework'
  s.version       = '1.0.0'
  s.summary       = 'A short description of MyFramework'
  s.homepage      = 'http://maxkalik.com'
  s.license       = { :type => 'MIT' }
  s.author        = { 'MyFramework' => '[email protected]' }
  s.source        = { :http => 'https://maxkalik.com/fameworks/MyFramework-v1.0.0.zip' }
  s.swift_version = '5.0'
  s.ios.deployment_target = '14.0'

  s.dependency 'CropViewController', '~> 2.6.1'
  s.dependency 'Kingfisher', '~> 2.6.1'

  s.vendored_frameworks = 'MyFramework.xcframework'
end

As you can see our podspec file is pretty straightforward but with some differences — if you would try to compare it with other ones which don’t use dependencies and are not as heavy as ours.

  • Source. We need to show where exactly our zip file is using :http => ‘path_to_public_storage’

  • Dependencies. First list all your dependencies and lock their versions.

  • Vendored Frameworks. We need to include a path to our final framework here. It supports both .framework and .xcframework bundles.

Now you can publish your podspec and test it. If you want to test your podspec in your own github repository then you should have a bit workaround:

  1. Make your repository with your podspec as private.
  2. Open a row representation of your podspec directly in github and copy the link from the browser. Why? because on each update the token in this link will be always unique otherwise you won’t be able to test the latest version of your podspec.
  3. Use the copied link in your test podfile and marked this type of source as podspec:

platform :ios, '15.0'

target 'ExampleProjectPods' do
  use_frameworks!

  pod 'MyFramework', :podspec => 'https://raw.githubusercontent.com/maxkalik/myframework-podspec/master/MyFramework.podspec?token=<TOKEN>'

end

It should work so let’s move forward.

3. XCFramework with XCFrameworks dependencies

Actually, this case is more obvious than you might think. All we have to do is to add this dependency to your framework project podfile and podspec. I tested this case using Intercom because it is as XCFramework itself.

Hence, your podfile should be updated just with adding Intercom dependency:

Pod::Spec.new do |s|
  s.name          = 'MyFramework'
  s.version       = '1.0.1'
  s.summary       = 'A short description of MyFramework'
  s.homepage      = 'http://maxkalik.com'
  s.license       = { :type => 'MIT' }
  s.author        = { 'MyFramework' => '[email protected]' }
  s.source        = { :http => 'https://maxkalik.com/fameworks/MyFramework-v1.0.1.zip' }
  s.swift_version = '5.0'
  s.ios.deployment_target = '14.0'

  s.dependency 'CropViewController', '~> 2.6.1'
  s.dependency 'Kingfisher', '~> 2.6.1'
  s.dependency 'Intercom', '~> 14.0.6'

  s.vendored_frameworks = 'MyFramework.xcframework'
end

As you can see it’s not needed to use vendored frameworks part to include Intercom XCFramework because it is just a dependency.

4. With dependencies that use XCFrameworks inside

Look section 3. Absolutely the same process.

5. XCFramework with dependency as XCFrameworks from special source path

Probably, I didn’t need to write the article above except for this part because when I encountered this case I couldn’t find a proper solution that could describe step-by-step how to do this. First of all this article I’m writing this for myself and then for you who are developing frameworks like me.

Ok. Stop complaining and let’s update our framework podfile.

platform :ios, '14.0'

source 'https://cdn.cocoapods.org/'
source 'https://github.com/passbase/cocoapods-specs.git'
source 'https://github.com/passbase/microblink-cocoapods-specs.git'

target 'YourFramework' do
  use_frameworks!

  pod 'CropViewController', '2.6.1'
  pod 'Kingfisher', '7.5.0'
  pod 'Intercom', '14.0.6'
  pod 'Passbase', '~> 2.7.0'

end

I added a new pod called Passbase. This pod is curious because besides of appending a dependency we need to add the source paths to where podspecs stores. But, what about podspec? You can offer just to add this line (as usual) to our dependencies list:

s.dependency ‘Passbase’, ‘~> 2.7.0’

When your client will try to install you new version of the framework, the process will be failed:

Why? Because obviously we have a dependency (Passbase) but the source path is not recognized. Of course, it will be cool just to update our podfile let’s say in this way:

s.dependency 'Passbase', '~> https://github.com/passbase/cocoapods-specs.git'

But it’s impossible. Cocoapod instructions doesn’t not allow it. I mean we cannot just point a specific dependency to the particular source path.

Workaround:

Yes, we need to do some weird manipulations to make it workable. So let’s look at pods folder of your framework and we will see there are actually 2 XCFramewoks: Passbase.xcframework and Microblink.xcframework

We need to copy these two XCFrameworks and put them together with your final XCFramework. When you put them all together we can make and zip them.

Also, we need to update podspec vendored_frameworks part:

s.vendored_frameworks = 'MyFramework.xcframework',
                          'Microblink.xcframework',
                          'Passbase.xcframework'

Actually, we are going to ship a zip file with 3 frameworks inside. Yes, it’s not an ideal solution but it works. I personally would like to see a possibility to include somehow specific sources in podspec file but it is what it is.

Potential Problems (Instead of conclusion)

Instead of a boring conclusion, which nobody cares about, I decided to write a list of potential problems and how to solve them by building xcframework as cocoapod. I think it will be more useful.

Problem 1: build.sh Permission Denied

MyFramework ./build.sh
zsh: permission denied: ./build.sh

Solution: We need to change access permission using chmod with a special flag +x to make the file executable.

Problem 2: Xcode 14 needs selected Development Team for Pod Bundles

MyFramework/Pods/Pods.xcodeproj: error: Signing for "ThirdPartyPod-ThirdPartyPodBundle" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'ThirdPartyPod-ThirdPartyPodBundle' from project 'Pods')

From Xcode 14 you can see this error and you have to select your Development team. It’s a not-so-big problem until you have a lot of bundles.

Solution:

This small script will help us to solve this problem:

post_install do |installer|
  installer.generated_projects.each do |project|
    project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings["DEVELOPMENT_TEAM"] = "Your Team ID"
         end
    end
  end
end

To learn your membership team id you can get it from your apple developer account in the Membership Details section (Team ID line).

Or in the same line, you can use this configuration:

config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'

Problem 3: Property is not a member type of class MyFramework.MyFramework

.../MyFramework.swiftinterface:6:34: error: 'SomeProperty' is not a member type of class 'MyFramework.MyFramework'
   public var direction: MyFramework.SomeProperty { get set }

Solution:

The best way to avoid this is if the names of modules in a framework are different and don’t coincide with the name of the framework itself. So, if you just started your framework from scratch — keep your public module name different. For example, our framework is calledMyFramework.xcframework so the name of your general module could be MyFrameworkMethods or so.

Problem 4: The path to your product is not found (the product folder in the archive is empty)

Solution:

Check flag: SKIP_INSTALL=NO in your build script — it will install your framework in the archive, in other words, your product folder won’t be empty.

Problem 5: Compiled module was created by a newer version of the compiler

Solution:

Check flag:BUILD_LIBRARY_FOR_DISTRIBUTION in your build script.

Problem 6: Build failed. A client uses the same dependencies which your framework already uses.

Solution
Briefly: Versions of the dependencies in a framework and in an app should be the same.


Detailed: Let’s say you build your framework with cocoapod Alamofire 5.5.0and a client app will use your framework and Alamofire 6.0.0, so this combination will cause a problem. In other words, you cannot build your app, and you won’t understand what is going on, because the error will be about symbols, or something else.

pod 'Intercom', '2.0.0'
pod 'Alamofire', '5.5.0'

Links


Written by maxkalik | Senior iOS Engineer at Triumph Labs. Launched WordDeposit and SimpleRuler Apps. Tech writer and public speaker.
Published by HackerNoon on 2023/02/17