How to manage the dependencies of binary frameworks on iOS
Nowadays, managing dependencies for iOS applications with Swift Package Manager, CocoaPods and Carthage is a well-documented topic. However, when it comes to the management of binary frameworks dependencies, there seems to be a lack of comprehensive resources.
One such case is the iOS Contentsquare SDK, distributed as binary framework to be integrated in the applications of our customers. When we decided to add SwiftProtobuf as a dependency of our SDK, we had to explore and compare multiple options before we finally identified the best solution, keeping in mind all the different package managers we had to support.
Along the way, we gained valuable insights and learned a few lessons we would like to share with you today!
Option 1: Copying all Swift files from SwiftProtobuf to our SDK
Often called “vendoring”, this is undoubtedly the safest solution but it has following drawbacks:
- It requires tedious work to implement.
We need to copy all the swift code from SwiftProtobuf to our SDK and then remove all the
public/open access
. - Version management is difficult. We need to check if a new version of SwiftProtobuf is released by ourselves from time to time.
- Last but not least, there’s limited opportunity for learning from this approach.
Option 2: Adding the dependency with Swift Package Manager
Naively, we can add SwiftProtobuf with Swift Package Manager into our SDK: it works very well except if this SDK is included into an application along with another SwiftProtobuf. Then we’ll see the following warning log:
We definitely want to avoid this for our customers so this option was excluded.
Option 3: Adding the dependency manually
Let’s create a sample Xcode project called Contentsquare using Framework as a template:
While adding dependencies to a Framework differs from adding them to an application, we can still use Carthage to build them manually. Here is how to add SwiftProtobuf
:
- Create a
Cartfile
and add the following dependency:
- Use the following command to build it.
Once we have the XCFramework, it’s the same process as adding dependencies to the application: we can drag and drop it to the “Frameworks and Libraries” section of our SDK target, except we need to select “Do Not Embed” for the Embed option, as nested bundles are not allowed by the App Store:
Since SwiftProtobuf is not embedded, we will have this error when we launch the application:
We need to fix this error differently for each dependency manager.
Carthage
Carthage is the easiest, we just need to add SwiftProtobuf
into the app’s Cartfile:
Then you can add SwiftProtobuf
to the “Frameworks and Libraries” section, and that’s it.
CocoaPods
CocoaPods supports adding dependencies to pods themselves, we just need to add the following line in the Podspec
file:
Now SwiftProtobuf
is declared as a dependency of Contentsquare, when we add pod 'Contentsquare', '~> 1.0'
to Podfile
to our App project, SwiftProtobuf
will be added automatically. This should work in most cases unless the configuration of the SwiftProtobuf
project is modified.
Known Issues and Workarounds
Changing IPHONEOS_DEPLOYMENT_TARGET
Some of us may change the IPHONEOS_DEPLOYMENT_TARGET
in the Podfile with following code:
The original IPHONEOS_DEPLOYMENT_TARGET
of SwiftProtobuf
is 9.0, changing this could cause a build error:
We need to revert this modification from SwiftProtobuf
to fix this:
Forcing static linking
We can use use_frameworks! :linkage => :static
to force static linking but when this option is enabled, we’ll have a runtime error:
This is because our Contentsquare Framework expect a dynamic SwiftProtobuf
Framework bundle at runtime. When linked statically, the dynamic bundle will not be generated.
We need to configure SwiftProtobuf
as a dynamic framework to fix this problem.
This is a temporary solution, we’ll provide a better one with Swift Package Manager.
Swift Package Manager
Swift Package Manager allows us to distribute binary frameworks. Here is an example using binaryTarget
:
But unlike target
, the binaryTarget
method doesn’t have a dependencies
parameter.
While there is no official support for this, here is a workaround used by Firebase:
As Swift Package Manager provides no easy way to link dependencies dynamically, we will have the same issue as with CocoaPods static linking:
To fix it, we can build our Contentsquare framework statically. This solution should also work for the previous CocoaPods problem.
Conclusion
Once all the problems were solved, we found that Option 3 works very well in production for all package management systems.
For more details, you can refer to the public documentation of our iOS SDK!