Adding Swift Package Manager Support to the Braintree iOS SDK

Sammy Cannillo
The PayPal Technology Blog
6 min readMay 24, 2021

By Susan Stevens and Sammy Cannillo

Background

The Braintree iOS SDK allows merchants to accept credit cards, debit cards, PayPal, Venmo, and a variety of alternate payment methods in their native iOS apps. It also provides fraud management tools to detect and prevent fraudulent transactions.

A package manager is a program that automates the process of installing, upgrading, and resolving dependencies required by the application. In the past, integrating the Braintree SDK required CocoaPods or Carthage. But with the release of Swift 5.0 and Xcode 11, there’s a new kid on the block: Swift Package Manager.

Motivation

Early on, our merchants expressed strong interest via GitHub Issues for Swift Package Manager. Unlike CocoaPods and Carthage, Swift Package Manager is developed by Apple, integrated directly into Xcode, and requires no additional software tools to use. It offers the promise of being an easy-to-use, first-class alternative solution.

The release of Swift 5.3, which introduces support for using binary frameworks as Swift Package dependencies, was essential for us since our SDK requires several binary dependencies from our fraud protection partners.

Challenges and Solutions

Problem: “File not found” errors

The Braintree iOS SDK is written largely in Objective C, and initially, Swift Package Manager couldn’t resolve our header imports. This resulted in a lot of “file not found” errors.

Solution: Update our file structure

The Braintree SDK is made up of several different frameworks; one for each payment method, and a few others for services like fraud protection. Using this approach, a merchant who doesn’t support Apple Pay, for example, doesn’t need to include code related to Apple Pay in their project.

Carthage users will see separate frameworks such as BraintreeApplePay, BraintreeCard, and so on, in their Carthage/Build folder. They can choose which frameworks to drag and drop into their projects. CocoaPods, on the other hand, builds a single Braintree framework based on the subspecs the merchant listed in their Podfile.

This means that when a file inside the SDK needs to import a public header from the SDK, it needs to look for it in two different places:

#if __has_include(<Braintree/BraintreeApplePay.h>)#import <Braintree/BraintreeApplePay.h> # CocoaPods#else#import <BraintreeApplePay/BraintreeApplePay.h> # Carthage#endif

Unfortunately, Swift Package Manager couldn’t find the files in either of these places. We didn’t want to complicate our import statements any more than we needed to, so we placed all of our public headers for each module inside a directory named after the module. This allowed Swift Package Manager to find the files in the same location as Carthage:

#import <BraintreeApplePay/BraintreeApplePay.h> # Carthage and SPM

Problem: Bugs in dependency resolution

We came across two major bugs in Swift Package Manager’s ability to resolve binary dependencies.

1) The first is the inability to locate the headers and source code of a package’s binary dependency upon archiving. Though the app can build and run, archiving fails and manifests with the following errors:

BinaryDependencyName.h file not found or No such module BinaryDependencyName

2) The second is a mysterious Symbols Tools Failed error message in the Xcode UI upon uploading to App Store Connect. Combing through Xcode’s logs, we identified that one of our .xcframework dependencies was built by packaging a static library (*.a). Turns out, using this type of xcframework with Swift Package Manager is a known issue.

Solution: Exposing binary dependencies and post-action run scripts

1) Our workaround for the archive issue is to expose each binary target as its own Swift Package product library. We then have to require our merchants include each binary dependency in their app via Swift Package Manager.

Here’s an example for how this looks in a Package.swift file:

let package = Package(    name: "Braintree",    products: [        .library(            name: "BraintreePayPal",            targets: ["BraintreePayPal"]),        .library(            name: "PayPalBinary",            targets: ["PayPalBinary"])    ],    targets: [        .binaryTarget(            name: "PayPalBinary",            path: "Frameworks/PayPalBinaryDependency.xcframework"),        .target(            name: "BraintreePayPal",            exclude: ["Info.plist"],            publicHeadersPath: ["Public"])    ])

2) Xcode is incorrectly processing xcframeworks packaged from static libraries by making an unnecessary copy of the static library (*.a) into the app’s Build directory. Our workaround is to require our merchants add a Build post-action to their app’s scheme. This removes the incorrectly duplicated static library.

rm –rf “${TARGET_BUILD_DIR}/${TARGET_NAME}.app/Frameworks/<StaticLibraryName>.a”

Problem: Carthage doesn’t support binary xcframeworks

Swift Package Manager requires binary dependencies to be packaged as .xcframework, but Carthage only supports the older .framework and static library .a formats.

Solution: Include two versions of each binary dependency

We updated the SDK to include two versions of each binary dependency — a .xcframework for Swift Package Manager and CocoaPods and a .framework or .a for Carthage. Luckily, each dependency manager expects frameworks to specify their dependencies in different ways, so providing multiple versions of our binary dependencies didn’t result in any conflicts. In Swift Package Manager, dependencies are specified in Package.swift. In CocoaPods, dependencies are specified in the podspec. For Carthage, we specify dependencies in the “Frameworks and Libraries” section of the target’s settings.

Unfortunately, we don’t have a .xcframework for every binary dependency just yet. More on that below.

Problem: Swift Package Manager doesn’t support various binary formats

Though Swift 5.3 introduces binary dependency support for Swift Package Manager, it only includes support for .xcframework binaries. This is a problem for us since we rely on our third-party partners to deliver their SDKs in the proper format. Our 3D Secure provider hasn’t been able to upgrade their SDK from a .framework to a .xcframework and doesn’t have the immediate bandwidth to commit to doing so. This prevents us from offering full SPM support for our module BraintreeThreeDSecure.

So, what do we do? Abandon Swift Package Manager support for now in hopes of an updated framework from this partner eventually? Offer Swift Package Manager support without one of our SDK’s most coveted features — 3D Secure Authentication? We came up with another workaround.

Solution: Runtime proxy checks with manual framework inclusion

Even if our merchants include the required .framework manually, SPM cannot properly resolve the binary dependency and will result in a compile-time error. So, we updated our BraintreeThreeDSecure module to dynamically check for the presence of this .framework dependency at runtime.

Here’s how:

1) We created proxy protocols for all the classes in the .framework dependency that the Braintree SDK relies on.

2) We used NSClassFromString to create instances of these framework classes at runtime and then cast them to our proxy protocol types. This allows us to instantiate classes and call methods from the third-party dependency without needing to know if its .framework is present at compile-time.

Note: This requires our merchants to include the .framework dependency manually in their app alongside the BraintreeThreeDSecure Swift Package. The Braintree SDK will manifest a run-time error if not properly included.

Here is a simplified code sample:

// Step 1: Create a protocol wrapping relevant classes and methods from the .framework@protocol BTProxyFrameworkClassA <NSObject>- (NSString *)getResponse;@end// Step 2: In your implementation code, use NSClassFromString to create instances of the framework class and then cast to your proxy protocol typeid<BTProxyFrameworkClassA> classA = (id<BTProxyFrameworkClassA>)[NSClassFromString(@”FrameworkClassA”) new];NSString *response = [classA getResponse];

Summary

While it hasn’t been without its challenges, especially for a larger, more complex SDK, we enjoy working with Swift Package Manager. We’ve already seen big improvements in Swift Package Manager, like the addition of support for binary frameworks, and there will likely be many more improvements to come.

One thing we’ve learned, though, is that maintaining support for three package managers is a challenge, especially with the current discrepancies between Carthage and SPM. Will Carthage catch up to the changes in the iOS ecosystem, making it easier for framework developers to support all three package managers? Or will the iOS community continue the transition over to Swift Package Manager, lessening the need for Carthage and perhaps even CocoaPods? We’ll be keeping a close eye on the evolution of iOS package managers.

--

--