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.
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.
Before diving into the process let’s prepare a list of how we are going to test our cocoapod XCFramework.
Let's go through all of these points and test all these cases together with me describing in detail the progress.
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
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.
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:
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.
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.
Look section 3. Absolutely the same process.
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.
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.
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.
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.
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'
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.
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.
Solution:
Check flag:BUILD_LIBRARY_FOR_DISTRIBUTION
in your build script.
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.0
and 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'