iOS Frameworks Explained: Static, Dynamic, and XCFrameworks
When building iOS applications, we often divide code into reusable modules. A networking layer, analytics SDK, or design system might be developed separately and shared across multiple applications.
In iOS, frameworks help us package and distribute this reusable code. But when working with frameworks, you’ll encounter three terms: static frameworks, dynamic frameworks, and XCFrameworks.
What is the difference? How do they work? And when should you use each one?
Let’s understand them by starting with the problems they solve.
1. What is an iOS framework?
Imagine you’re building three applications:
- A banking application
- A shopping application
- A travel application
All three need networking functionality to communicate with their respective backends.
Instead of implementing networking separately in each application, you create a reusable module called NetworkKit.
Banking App ─────┐
│
Shopping App ───┼────> NetworkKit
│
Travel App ──────┘This makes it easier to maintain and reuse networking functionality across applications.
A framework can package several things:
- Compiled code.
- Public interfaces and headers.
- Resources such as images, fonts, and asset catalogs.
- Supporting files and metadata.
However, a framework’s packaging format does not, by itself, tell you how its code will be linked.
That’s where static and dynamic frameworks come in.
2. Static Frameworks: Include the Code in Your App
A static framework provides code that is linked into the application during the build process.
Imagine NetworkKit contains code for:
- Sending HTTP requests.
- Handling responses.
- Decoding JSON.
- Managing network errors.
When your application links this code, the required implementation becomes part of the final application executable.
How does it work?
NetworkKit
|
| Build and Link
v
Banking App Executable
|
v
Application RunsThe application doesn’t need to load NetworkKit as a separate dynamic framework at runtime.
The linker incorporates the required object code into the application executable.
Advantages
- Avoids a separate dynamic-library loading step for the statically linked code.
- Can simplify runtime dependency packaging.
- Works well for libraries that don’t need to exist as separate dynamic libraries at runtime.
Trade-offs
- The executable typically becomes larger because it contains the linked implementation.
- If multiple application executables incorporate the same library, its code may be duplicated across them.
- Updating the library generally requires rebuilding and redistributing the application.
Static linking also typically reduces some launch-time work because the linked implementation doesn’t need to be loaded as a separate dynamic library.
However, the actual impact depends on the dependency graph, code size, build settings, and application behavior.
What about framework resources?
One important detail: linking a static framework doesn’t automatically mean all its resources become part of the application executable.
For example, if NetworkKit contains images, fonts, or asset catalogs, those resources must also be packaged and made accessible to the application.
The build system and framework integration determine how these resources are copied and accessed at runtime.
When would you use it?
Suppose you have a self-contained utility library containing:
- Date formatting.
- Input validation.
- Helper functions.
- Common business logic.
If you want to avoid a separate dynamic dependency and the library has a straightforward dependency graph, static linking is often a sensible choice.
However, audit the dependencies first.
If two static libraries both incorporate the same underlying implementation, you may encounter duplicate symbols or unnecessary code duplication.
The key idea: Static linking simplifies some runtime concerns, but you need to understand what code is being incorporated into the application.
3. Dynamic Frameworks: Load the Code as a Separate Library
Now consider an application that integrates a large media SDK.
A dynamic framework keeps its implementation in a separate dynamic library instead of incorporating that implementation into the application executable in the same way as a static library.
How does it work?
Banking App
|
v
Dynamic Library Reference
|
v
MediaKit.frameworkThe application contains a reference to the framework, and iOS loads the required dynamic library when the application runs.
For an iOS application, a third-party dynamic framework normally needs to be embedded in the application bundle and signed appropriately.
Advantages
- Keeps the framework’s implementation separate from the main executable.
- Can allow multiple components within the same process to use the same loaded dynamic library.
- Provides a separate binary boundary that can be useful for modular development and distribution.
One important clarification: separate iOS applications cannot share an embedded third-party framework simply because they are developed by the same company. Each application must package its own required framework dependencies.
Trade-offs
- The main executable is typically smaller than it would be if it incorporated the same implementation statically.
- Dynamic loading and symbol resolution add work during application startup or when the library is loaded.
- Framework embedding, signing, and runtime dependencies must be handled correctly.
The important distinction is that the main executable may be smaller, but the complete application bundle does not necessarily become smaller.
The dynamic framework still has to be delivered with the application, and the actual launch-time impact depends on the application.
When would you use it?
Suppose your company develops a reusable SDK consumed by several applications and wants to distribute it as a separately linked binary.
A dynamic framework may be appropriate when that binary boundary is useful and your team can manage the associated runtime dependencies.
But don’t choose dynamic linking simply because the main executable looks smaller.
Consider the complete application, its startup behavior, and how the framework will be deployed.
The key idea: Dynamic linking separates the implementation from the main executable, trading some runtime loading work for that separation.
4. XCFrameworks: Distribute Binaries for Different Platforms
Now consider a different problem.
You’ve created NetworkKit and want other developers to integrate it into their applications.
You build it for a physical iPhone, and everything works. But a developer tries to use the same binary in an Apple Silicon simulator and encounters a compatibility error.
Why?
A physical iPhone and an Apple Silicon simulator can both use the ARM64 architecture, but they target different platforms.
A device binary isn’t automatically compatible with the simulator just because both use ARM64.
Why did this become a compatibility problem?
Before Apple Silicon, iOS devices used ARM64 while Intel-based simulators used x86_64.
Since these architectures were different, a universal, or fat, binary could contain both architecture slices.
With Apple Silicon, simulators also began using ARM64.
Now, both an iOS device and an iOS simulator could require the same CPU architecture but target different platforms.
A fat binary identifies slices by CPU architecture, not by the complete platform environment.
Therefore, architecture alone is insufficient to distinguish these binaries.
This made platform-aware binary packaging essential.
How XCFrameworks solve the problem
An XCFramework packages the appropriate binary variants into one distributable bundle.
NetworkKit.xcframework
|
├── iOS device variant
|
└── iOS Simulator variantEach variant contains the binary built for its respective platform.
Get VINODH KUMAR’s stories in your inbox
Join Medium for free to get updates from this writer.
Xcode examines the build destination and selects the appropriate variant.
Advantages
- Packages binaries for multiple platforms and variants in one bundle.
- Simplifies integration for developers consuming a compiled SDK.
- Avoids relying on architecture-only combinations of device and simulator binaries.
Trade-offs
- You must build and validate the required variants.
- Including multiple variants increases the size of the distributed package.
- It doesn’t automatically manage dependencies or guarantee API compatibility.
When would you use it?
Suppose you’re building a proprietary PaymentKit SDK that other teams need to integrate without receiving its source code.
You can distribute it as an XCFramework containing the necessary device and simulator variants.
An important distinction:
An XCFramework is a distribution format, not a linking strategy.
It can package static or dynamic frameworks and libraries.
5. Building an XCFramework: A Practical Example
Let’s understand how you would create an XCFramework using Xcode’s build tools.
Suppose you have a framework target named NetworkKit.
The goal is to produce a single distributable bundle containing:
- An iOS device framework.
- An iOS Simulator framework.
Step 1: Archive the iOS device framework
xcodebuild archive \
-scheme NetworkKit \
-destination "generic/platform=iOS" \
-archivePath "./build/NetworkKit-iOS" \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YESThis creates an archive containing the framework built for physical iOS devices.
Step 2: Archive the iOS Simulator framework
xcodebuild archive \
-scheme NetworkKit \
-destination "generic/platform=iOS Simulator" \
-archivePath "./build/NetworkKit-Simulator" \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YESThis creates another archive containing the framework built for the iOS Simulator.
Notice that both builds can use ARM64, but they target different platforms.
Step 3: Combine the framework variants
Now use -create-xcframework to combine both framework variants.
xcodebuild -create-xcframework \
-framework "./build/NetworkKit-iOS.xcarchive/Products/Library/Frameworks/NetworkKit.framework" \
-framework "./build/NetworkKit-Simulator.xcarchive/Products/Library/Frameworks/NetworkKit.framework" \
-output "./build/NetworkKit.xcframework"You now have:
NetworkKit.xcframework
|
├── ios-arm64
|
└── ios-arm64_x86_64-simulatorThe actual variant directories and architectures depend on your build configuration and supported destinations.
What does BUILD_LIBRARY_FOR_DISTRIBUTION do?
You might have noticed this build setting:
BUILD_LIBRARY_FOR_DISTRIBUTION=YESThis enables module stability and library evolution support for Swift frameworks.
It is particularly useful when distributing compiled Swift frameworks to consumers who may build using different compatible compiler versions.
It is not what makes a framework an XCFramework. The XCFramework is created by the final -create-xcframework command.
The key idea: Building separate platform variants and combining them into an XCFramework makes binary distribution much easier for SDK consumers.
6. Understanding Framework Dependencies
There is another important consideration when distributing frameworks: dependencies.
An XCFramework packages the binary variants you provide. It doesn’t automatically bundle and resolve every dependency those binaries need.
Let’s understand this with an example.
Suppose PaymentKit depends on another library called CryptoKitWrapper.
PaymentKit.xcframework
|
v
CryptoKitWrapperWhat happens when you distribute PaymentKit?
Scenario 1: A static framework depends on another static library
If a static framework depends on another static library, the consuming application may need to link that dependency separately.
If the required implementation isn’t available to the linker, you may encounter undefined symbol errors.
Scenario 2: A dynamic framework depends on a static library
A dynamic framework that depends on a static library must incorporate the required static code into its own binary during the framework’s build.
If the application also links the same implementation independently, duplicate symbols may result.
Scenario 3: Swift Package Manager and binary targets
Swift Package Manager supports binary frameworks through binaryTarget.
For example:
.binaryTarget(
name: "NetworkKit",
path: "NetworkKit.xcframework"
)However, a binaryTarget cannot declare its own target dependencies on other Swift packages.
The surrounding package can declare package dependencies and use source targets to coordinate them with the binary target.
The binary’s transitive dependencies still need to be managed by the package author.
The key idea: XCFrameworks solve binary distribution across platforms. They do not replace dependency management.
7. Static vs. Dynamic vs. XCFramework
Let’s compare all three concepts
The important distinction
Static and dynamic describe how code is linked and loaded.
XCFramework describes how compiled binaries are packaged and distributed.
You can even create a static XCFramework.
For example:
NetworkKit.xcframework
|
├── iOS device variant (Static)
|
└── iOS Simulator variant (Static)This is perfectly valid.
The XCFramework doesn’t determine whether the code is statically or dynamically linked. It simply packages the platform-specific binary variants.
8. How Should You Choose?
Start with the engineering requirement rather than the terminology.
Choose static linking when:
- You want the implementation incorporated into the client.
- You don’t need a separate dynamic library at runtime.
- You want to avoid additional dynamic framework loading.
- The dependency graph is straightforward and manageable.
Choose dynamic linking when:
- A separate runtime library is useful for modularity or distribution.
- You want to maintain a separately linked binary boundary.
- You’re prepared to manage framework embedding, signing, and runtime dependencies.
Choose an XCFramework when:
- You need to distribute compiled code.
- Your SDK supports multiple Apple platforms.
- You need separate device and simulator variants.
- You want a single distributable bundle that Xcode can consume across supported destinations.
Remember, these options are not mutually exclusive.
For example, you can create a static framework and distribute its device and simulator variants through an XCFramework.
The decision involves more than performance. Consider binary size, dependency ownership, startup behavior, code distribution, and platform compatibility.
Conclusion
Understanding iOS frameworks is less about memorizing definitions and more about recognizing which problem each option solves.
- Static linking incorporates code into the application executable, typically increasing its size while avoiding a separate dynamic-library load for that code.
- Dynamic linking keeps the implementation in a separate library, typically reducing the main executable’s size while introducing runtime loading and packaging considerations.
- XCFrameworks make it easier to distribute the appropriate binary variants to developers building for different platforms.
They address different engineering concerns, and understanding those differences helps you make better architectural decisions.
Once you understand these distinctions, you can choose an approach based on your application’s architecture, startup requirements, dependency graph, and distribution needs rather than simply following a convention.






