INTRODUCTION: How Did We Discover This iOS Linking Anomaly in Production?
While working on a recent project for a fast-growing FinTech platform, our team was tasked with building a white-label mobile SDK. This SDK needed to securely process transaction data and integrate seamlessly into various third-party merchant applications. Given the strict security and distribution requirements, we decided to distribute the SDK as an XCFramework.
During the integration phase, we encountered a puzzling situation. Our primary SDK (let’s call it MySDK) relied heavily on a separate internal cryptographic utility SDK (DependencySDK), which was compiled as a static library. When we configured MySDK as a Dynamic Library and dropped it into a test host app, everything built and ran perfectly without needing to explicitly add DependencySDK to the host app. However, when we switched MySDK to a Static Library to meet a specific client’s performance constraints, the host app build failed spectacularly with a flurry of Undefined symbols errors pointing directly at our dependency.
In mobile engineering, binary linkage behaviors often catch developers off guard. The counter-intuitive assumption is that static libraries encapsulate all their dependencies, while dynamic libraries require explicit runtime inclusion. Discovering the exact opposite behavior in production forced us to dive deep into the Mach-O binary formats, the Xcode build system, and the intricacies of static versus dynamic linking. We are sharing this journey so that other engineering leaders and developers can avoid the same architectural pitfalls. Whether you build internal tools or hire software developer teams to deliver enterprise SDKs, understanding this linkage mechanic is crucial.
PROBLEM CONTEXT: Why Do XCFramework Dependencies Behave Differently?
The core of our business use case was to provide a streamlined, drop-in SDK for merchants. We wanted to distribute a single MySDK.xcframework file to minimize integration friction. Requiring clients to juggle multiple internal dependency frameworks manually was out of the question.
Here was the architecture of our framework stack:
- DependencySDK.xcframework: Built as a Static Library. It contained the core logic we didn’t want exposed.
- MySDK.xcframework: Built as the public-facing wrapper. It imported and utilized
DependencySDK.
When testing integration, we observed two distinct cases:
Case 1: MySDK as a Dynamic Framework
We set the Mach-O Type to Dynamic Library. We generated MySDK.xcframework and dragged only that framework into the host application. The app built, linked, and ran successfully. The host app never knew DependencySDK existed.
Case 2: MySDK as a Static Framework
We changed the Mach-O Type to Static Library. We generated the new MySDK.xcframework and dragged it into the host application. The build instantly failed during the linking phase. The compiler threw Undefined symbols for architecture arm64, specifically complaining about missing classes and functions from DependencySDK.
This presented a massive architectural concern: If static libraries are meant to be self-contained, why was our dynamic framework successfully hiding the dependency, while our static framework was leaking unresolved symbols to the end user’s app?
WHAT WENT WRONG: Why Did the Static SDK Fail While the Dynamic SDK Succeeded?
The root cause of this failure comes down to a fundamental misunderstanding of what static and dynamic libraries actually are at the compiler level. When engineering teams build mobile architectures, they often visualize libraries as folders that “hold” code. In reality, the Mach-O Type completely dictates the linking phase.
A Dynamic Library is a fully linked, finished binary. When Xcode built our dynamic MySDK, the linker resolved all references. Because DependencySDK was static, the linker physically copied the necessary object code from DependencySDK and baked it directly into the MySDK dynamic binary. The resulting MySDK binary contained both its own code and the dependency’s code. Therefore, when the host app linked against MySDK, there were no unresolved symbols. The dynamic framework successfully swallowed the static dependency.
A Static Library, on the other hand, is not a fully linked binary. It is merely an archive (a .a file) of unlinked object files (.o). When Xcode built our static MySDK, it essentially just compiled the Swift/Objective-C files into object files and zipped them up. It did not perform a linking step. It did not embed DependencySDK. It simply left pointers saying, “I need these cryptographic functions eventually.”
When the host app tried to build, the linker finally kicked in to merge the app’s code with the static MySDK. The linker read the object files, saw the pointers requesting DependencySDK, searched the app target for them, and failed because DependencySDK was never provided to the app target. The result? Undefined symbols.
HOW WE APPROACHED THE SOLUTION: What Were Our Options to Fix the Undefined Symbols?
To resolve this, we mapped out our technical requirements: we needed a single .xcframework artifact, zero external dependencies for the client, and no exposed internal cryptography logic. We analyzed several approaches. This rigorous evaluation phase is standard practice when clients hire iOS developers for enterprise SDKs through our dedicated models.
Did We Consider Forcing the Host App to Link Dependencies Explicitly?
Our first thought was simply to distribute both MySDK.xcframework and DependencySDK.xcframework and instruct clients to add both to their Xcode projects. While technically sound, this completely violated our business requirement for a frictionless, single-artifact integration. It also risked exposing internal architecture to external clients.
Did We Consider Merging Static Libraries Using Libtool?
Since a static library is an archive, we considered unpacking both MySDK and DependencySDK object files and merging them into a single “fat” static library using Apple’s libtool command line utility.
While this is a known hack in the iOS community, it is notoriously brittle. It frequently leads to duplicate symbol errors if the host app happens to use a similar underlying open-source library, and maintaining custom bash scripts to manage binary merging creates a significant maintenance burden for CI/CD pipelines.
Did We Consider Using a Package Manager Like Swift Package Manager (SPM)?
We could have distributed MySDK via SPM and defined DependencySDK as a binary target dependency in the Package.swift manifest. SPM would seamlessly fetch and link both binaries into the client’s host app. However, our client base included enterprise banks relying on legacy build systems that restricted external package resolution. We had to support direct drag-and-drop integration.
Did We Consider Statically Linking the Dependency Inside a Dynamic Framework?
This became our optimal path. By configuring MySDK as a Dynamic framework, we could leverage the build-time linker to statically consume the DependencySDK. The resulting dynamic .xcframework would be a single, portable, self-contained binary. This approach offers the best developer experience (DX) for the end client, which is exactly what we advise when organizations hire app developer to create a mobile app or SDK.
FINAL IMPLEMENTATION: How Did We Correctly Link and Distribute the XCFramework?
We executed the dynamic embedding strategy with precise build configurations. Here is the step-by-step breakdown of the technical fix that stabilized our pipeline.
First, we ensured that DependencySDK remained a pure Static Library.
Second, we navigated to the Build Settings of MySDK and enforced the following configurations:
// MySDK Build Settings MACH_O_TYPE = mh_dylib // Dynamic Library DEAD_CODE_STRIPPING = YES STRIP_LINKED_PRODUCT = YES
Third, in the Frameworks and Libraries section of MySDK‘s target, we added DependencySDK.xcframework. Crucially, we set the embedding option to Do Not Embed. Since DependencySDK is static, setting it to “Embed” would cause Xcode to needlessly copy the raw archive into the final framework bundle, bloating the size. “Do Not Embed” ensures the linker copies the compiled symbols directly into the executable binary instead.
Next, we updated our CI/CD generation script to export the XCFramework:
# Generate the iOS Simulator Archive xcodebuild archive -scheme MySDK -destination 'generic/platform=iOS Simulator' -archivePath ./build/MySDK-iphonesimulator.xcarchive BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO # Generate the iOS Device Archive xcodebuild archive -scheme MySDK -destination 'generic/platform=iOS' -archivePath ./build/MySDK-iphoneos.xcarchive BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO # Stitch them into a single XCFramework xcodebuild -create-xcframework -framework ./build/MySDK-iphonesimulator.xcarchive/Products/Library/Frameworks/MySDK.framework -framework ./build/MySDK-iphoneos.xcarchive/Products/Library/Frameworks/MySDK.framework -output ./build/MySDK.xcframework
Validation Steps: We validated the resulting binary using the nm tool to ensure the cryptographic symbols from DependencySDK were present within the dynamic MySDK binary, and that no undefined symbols were leaking.
nm -g ./build/MySDK.xcframework/ios-arm64/MySDK.framework/MySDK | grep "DependencySDK"
By enforcing dead code stripping, we also ensured that only the specific functions MySDK actively used from DependencySDK were baked in, drastically reducing the final payload size.
LESSONS FOR ENGINEERING TEAMS: What Can Other Teams Learn About iOS Binary Linkage?
Our deep dive into Mach-O binaries highlighted several critical lessons that technical leaders should enforce within their mobile engineering teams:
- Static means Archive, Dynamic means Linked: Never assume a static library is a complete package. It is an unlinked collection of code that defers symbol resolution until the final app build. Dynamic libraries are complete, linked entities.
- Use Dynamic Frameworks for Vendor Distribution: If you are required to provide a single, drag-and-drop artifact that hides internal dependencies, wrapping static dependencies inside a dynamic XCFramework is the cleanest, most reliable approach.
- Beware of the “Embed” Setting: When adding a static library to a dynamic framework in Xcode, always select “Do Not Embed”. The static code is linked into your binary at build time; embedding it simply wastes space and can trigger App Store rejection for invalid bundle structures.
- Enable Module Stability: Always set
BUILD_LIBRARY_FOR_DISTRIBUTION=YESwhen generating XCFrameworks. This ensures your Swift framework maintains ABI and module stability across different compiler versions. - Master Command Line Diagnostics: Equip your team with the knowledge to use tools like
nm,otool, andlipo. These utilities are indispensable for inspecting binaries, verifying architectures, and diagnosing undefined symbols without relying on Xcode’s opaque error logs. - Align Strategy with Enterprise Context: When you hire swift developers for mobile architecture, ensure they understand not just how to write code, but how build systems behave under different enterprise constraints, such as legacy CI pipelines or strict security policies.
WRAP UP: Are You Ready to Optimize Your iOS SDK Delivery?
Resolving the mystery of undefined symbols in static frameworks ultimately improved our SDK’s delivery footprint and saved countless hours of integration frustration for our client’s end-users. By understanding the low-level differences between dynamic linking and static archiving, we architected a single, robust XCFramework that securely encapsulated all dependencies. The ability to navigate these complex build system quirks is a hallmark of mature engineering teams. If your organization is facing similar architectural bottlenecks or looking to scale mobile delivery securely, contact us to explore how our experienced engineering teams can drive your next project to success.
Social Hashtags
#iOSDevelopment #Swift #XCFramework #Xcode #MobileDevelopment #iOSSDK #AppleDeveloper #SoftwareEngineering #SwiftDeveloper #iOSArchitecture #MachO #AppDevelopment #StaticLibrary #DynamicFramework #TechBlog
Frequently Asked Questions
During the build of the dynamic framework, the linker copied all the necessary object code from the static dependency directly into the dynamic framework's executable binary. Because the dependency's code was embedded at compile time, the host app did not need the separate dependency framework.
No, Xcode is not putting a .framework folder inside your dynamic framework. Instead, the linker is statically linking the object files (the compiled C/C++/Swift code) directly into your single Mach-O binary file.
Only if you merge the static dependency's object files into your primary static library using a command-line tool like libtool. However, this process is complex, error-prone, and not natively supported by a simple Xcode build setting.
For single-artifact distribution without relying on a package manager, the industry best practice is to build your public-facing SDK as a Dynamic Framework and link all private, internal dependencies as Static Libraries into it. This produces a clean, self-contained binary.
It will increase the size of the dynamic framework, but you can mitigate this by enabling Dead Code Stripping (DEAD_CODE_STRIPPING = YES). This ensures the compiler only includes the specific pieces of code from the dependency that your SDK actually executes, rather than the entire library.
Success Stories That Inspire
See how our team takes complex business challenges and turns them into powerful, scalable digital solutions. From custom software and web applications to automation, integrations, and cloud-ready systems, each project reflects our commitment to innovation, performance, and long-term value.

California-based SMB Hired Dedicated Developers to Build a Photography SaaS Platform

Swedish Agency Built a Laravel-Based Staffing System by Hiring a Dedicated Remote Team

















