Packaging Libraries in iOS: A Comprehensive Guide

Understanding the premise

Software Development Kits (SDKs) are the lifeblood of development, offering a treasure of pre-packaged libraries, tools, and resources that empower developers to craft rich and feature-packed applications. However, for those new to distributing SDKs in iOS, the path can be filled with uncertainty.

We have shipped 100s of iOS SDK builds in the form of our WebRTC clients for customers looking to integrate real time communications within their applications. With that experience, this technical blog post is tailored for iOS developers and aims to illuminate the nuances of SDK distribution, including key concepts, best practices, and tools. With this knowledge, developers can unlock the full potential of their SDKs and ensure seamless integration experiences.

Static vs. dynamic libraries and frameworks in iOS

Seasoned iOS developers often grapple with crucial decisions regarding system frameworks, packaging their code, and integrating third-party components. Among these decisions, choosing between static and dynamic libraries or frameworks is pivotal, making profound implications for application performance and resource management.

Implications on app size and launch time

The choice between static and dynamic libraries or frameworks carries significant weight regarding your app's binary size and launch time.

Summary of static Vs. dynamic linking

Here's a concise summary of how static and dynamic linking impacts various facets of your application:

Facets Static Linking Dynamic Linking
App Size Large app bundle size Smaller app bundle size
App Launch Time Faster load time Slower load time
Safety Scrutinised and copied at build time Risk of runtime glitches, potential for runtime crashes
Deployment In static linking, the SDK or library is bundled within the single app binary, and the entire library's code becomes an integral part of the app's binary executable. App references the library at runtime, and the operating system is responsible for loading the required library when the app is launched or when the specific library functions are first called
Debugging Easier to debug as all code is available Harder to debug as code may not be available at runtime
Memory Usage apps that are statically linked tend to have less memory usage during runtime. may have slightly more memory usage during runtime
Flexibility Updates to the SDK or library require app recompilation and release because the entire library is bundled with the app's binary during compilation, preventing separate updates. Library updates can be made independently of the app, offering flexibility and allowing users to benefit from updates without app recompilation.

When to use dynamic linking

While statically linked modules proffer a smaller app size and accelerated loading times, dynamic linking has its own set of compelling use cases:

  • Multiple static modules depending on the same module: When your app comprises multiple static modules that lean on a common module, you might encounter warnings about duplicate symbols at runtime. Transitioning the shared module to a dynamic one can effectively mitigate this issue.
  • iOS increased app launch time with many dynamic libraries/frameworks: Loading numerous third-party dynamic libraries or frameworks on iOS can lead to prolonged app launch times. Vigilant monitoring and optimisation efforts are essential to upholding app launch performance.

Static/dynamic with different integration techniques

To make well-informed decisions about linking, a profound understanding of how various dependency managers handle linking behaviour is imperative:

  • Own targets or projects linked directly: Exert precise control over the linking behaviour of targets within your repository or external repositories by fine-tuning Build Settings. A simple adjustment to the MACH_O_TYPE Build Setting allows you to toggle between static library and dynamic library.
  • CocoaPods: By default, CocoaPods constructs and links dependencies as static libraries. The introduction of use_frameworks! in your Podfile enables the construction of dynamic frameworks. Moreover, :linkage => :static can be employed to shape dependencies as static frameworks.
  • Swift Package Manager (SPM): SPM, by default, fabricates dependencies as static libraries, offering minimal control over linking behaviour. However, if you are the custodian of a package, you can specify type: .dynamic within your Package.swift file to fashion a dynamic package.
  • Carthage: In its default configuration, Carthage leans towards using dynamic frameworks for dependencies. Nevertheless, you retain the flexibility to configure it to construct and link them statically when circumstances dictate such an arrangement.

Understanding different formats

In the realm of iOS development, many formats for packaging libraries and resources exist. Each format serves distinct purposes, and as an expert iOS developer, comprehending their nuances is indispensable. Here's an exploration of some essential formats:

xcframework

XCFramework is a relatively recent addition to Apple's arsenal of formats. It is a versatile container meticulously designed for packaging frameworks for diverse platforms and architectures into a single, harmonious bundle. Embracing XCFrameworks streamlines the distribution of binary frameworks, ensuring harmonious coexistence across various Apple devices and processor architectures. Adopting XCFrameworks bestows developers with the gift of streamlined development, reduced integration complexities, and enhanced application performance.

Framework

The classic framework format remains a stalwart choice for packaging code and resources in iOS development. Frameworks offer a structured habitat for your codebase, gracefully accommodating header files, binaries, and resources. They advocate the virtues of encapsulation and modularity, simplifying the process of integration and maintenance of your code. Frameworks wear the dual hats of either static or dynamic, contingent upon whether the code binds at compile time (static) or runtime (dynamic).

.a (Static Library) and .o (Object File)

.a files, heralded as static libraries, house compiled code snugly woven into the fabric of your application at compile time. These libraries accompany your app's binary, bestowing it with a petite binary size and promising accelerated startup times. .o files, or object files, inhabit the realm of intermediate compilation units, wielding the potential to unite and give birth to static libraries or dynamically linked frameworks. Distinguishing between the use cases of static libraries and object files is pivotal for optimising your app's performance and memory footprint.

.dylib (Dynamic Library)

The world of .dylib files beckons dynamic libraries, extending an invitation for their ad hoc arrival at runtime as your app springs into existence. Dynamic libraries usher in a measure of flexibility in code sharing but might nudge your app's binary size northward. Dynamic libraries traditionally find their calling in system frameworks and shared system components. Handling them with care and vigilance is a requisite, for any slip-up in configuration or inclusion can usher in the Spector of runtime crashes.

Universal Framework

Universal frameworks, the "Jack of all trades" among formats, don the cloak of versatility. They are a specialised framework format engineered to accommodate multiple architectures and platforms under one sprawling roof. This format performs an invaluable service, simplifying the distribution of cross-platform libraries. Developers can present a single binary, an embodiment of unity, capable of functioning seamlessly across an array of iOS devices and processor architectures.

Swift Package Manager

As a seasoned iOS developer, you're no stranger to the capabilities wielded by Swift Package Manager (SPM) when it comes to dependency management. Within the labyrinthine corridors of SPM, two pivotal concepts demand your attention: .binaryTarget, .target, and the strategic utilization of linkerSettings.

.binaryTarget

Introduced as a breath of fresh air, .binaryTarget assumes the mantle of a feature within Swift Package Manager designed explicitly to streamline the integration of binary dependencies. Binary dependencies are pre-compiled libraries or frameworks presented by third-party sources, emerging as swift and efficient companions for integration. Here's the lowdown:

  • Efficient integration: With the declaration of a .binaryTarget, Swift Package Manager orchestrates retrieving a pre-compiled binary from a designated source, whether a Git repository or a URL. This streamlined approach expedites integration, ushering inconvenience.
  • Platform-agnostic brilliance: Binary targets shine as platform-agnostic stars, casting their glow across diverse platforms and architectures, including iOS, macOS, and beyond. This trait is especially beneficial for those engaged in cross-platform development endeavors.
  • Version control vigilance: Binary targets, operating in the realm of pre-compiled artifacts, dwell outside the confines of direct version control within your package. Instead, the version or tag of the binary dependency takes center stage within your Package.swift file.
  • Swift harmony: Ensure the selected binary target aligns with your project's Swift version for harmony and compatibility. A mismatch in Swift versions can sow the seeds of discord.

.target

In stark contrast to the pragmatic elegance of .binaryTarget, .target emerges as the go-to directive for source-based dependencies. Source-based packages carry their source code, ready to undergo the rites of compilation upon integration into your project. Vital insights into this concept include:

  • Source code integration: Invocation of a .target commands Swift Package Manager to embark on a journey of source code retrieval, cloning the package's source code and forging it into your project. This process offers the boon of customization, enabling you to mold or modify the package as per your requirements.
  • Version control ascendancy: Source-based packages ascend to prominence as the champions of direct version control within your project's repository. This translates into the power to wield influence over the package's code, affording the liberty to make modifications as needed.
  • Swift compatibility: As with binary targets, extending a cordial handshake of compatibility between your project's Swift version and the chosen source-based package is paramount. Avoidance of mismatched Swift versions can be your shield against compatibility conundrums.
  • The web of dependency: Source-based packages often weave a web of dependencies, crafting a sprawling tapestry of interconnected packages. Swift Package Manager rises to the challenge, orchestrating the management of this intricate dependency graph, ensuring that all required packages partake in the grand symphony of integration.

linkerSettings

linkerSettings, an entity of significance within the grand configuration of Swift Package Manager, entrusts you with the reins of control over linker flags and settings that govern the orchestration of your targets. Its importance is not to be underestimated:

  • Fine-grained command: linkerSettings stands as your herald, bearing the mandate of fine-grained control over the linking process. It paves the way for the specification of linker flags, search paths, and sundry settings, exerting a profound influence on how the package melds with your project.
  • Integration tailoring: Instances may arise where a package exhibits a penchant for specific linker flags or settings. Enter linkerSettings; this savior allows you to fashion an integration that gracefully accommodates the package's unique requirements.
  • Mitigating linking conflicts: In the integration, where multiple packages jostle for space, conflicts on the frontiers of linking can rear their heads. linkerSettings steps in as the peacemaker, offering the means to navigate these conflicts with poise and elegance, ensuring a harmonious integration experience.
  • Compatibility crusade: To preserve the sanctity of your project, heed the call of compatibility. Scrutinize the compatibility of linker flags and settings with your project's Swift version and platform. Mismatched configurations have the power to disrupt the tranquil flow of runtime.

The role of checksums in SPM

Checksums, those guardians of integrity and security, play a pivotal role within the domain of Swift Package Manager (SPM), specifically in the realm of package management. They serve as sentinels tasked with verifying the authenticity and integrity of external dependencies before ushering them into your project.

  1. Package resolution: Your project's Package.swift file hosts declarations of dependencies. When summoned, the Swift Package Manager embarks on a mission to retrieve the package manifest, an essential dossier housing information about the package and its version.
  2. Download and check: The saga continues with downloading the package's source code or binary artifact. Simultaneously, Swift Package Manager, armed with diligence, fetches the checksum linked to the package's version from a trustworthy source, often the package repository.
  3. Verification: As the download completes, Swift Package Manager commences an expedition into checksum calculation. The calculated checksum stands face-to-face with its repository-forged counterpart, and only harmony, represented by a perfect match, rings true. A matching checksum signifies that the downloaded package mirrors the precise form and content expected by the package repository.

Mergeable libraries (New in Town!)

Apple announced Mergeable libraries in WWDC23, the unsung heroes sometimes cloaked in the monikers of "umbrella frameworks" or "universals," which is the fusion of multiple frameworks or libraries into a singular, cohesive framework. This gives a leaner, meaner, and more efficient application binary. Let's shine a light on the critical facets of mergeable libraries:

1. Reduction in binary size

The most prominent jewel in the crown of mergeable libraries is the substantial reduction in the size of your application binary. By amalgamating multiple frameworks into a singular entity, developers surgically excise redundant code and resources, rendering the binary trim and sleek. This trimness finds its true value in mobile applications, where app size directly influences download times and device storage.

2. Streamlined maintenance

Mergeable libraries don the mantle of the custodian of dependencies, simplifying the labyrinthine maintenance process. Instead of juggling several individual libraries, each with its own versioning and update cycle, developers are entrusted with the guardianship of a solitary, consolidated library. This harmonisation streamlines the update process and curtails the risk of version conflicts and compatibility conundrums.

3. Improved load/launch times

Diminished binary sizes usher in improved app launch times. With fewer resources to load into memory, the app leaps to life more swiftly, embellishing the user experience with the gift of alacrity. Reduced load times are particularly invaluable in scenarios where instant access to the application is not just a luxury but an expectation.

4. Cross-platform compatibility

Mergeable libraries possess the unique capability of accommodating multiple platforms and architectures, transforming them into the darlings of cross-platform development. In the hands of adept developers, a single library can extend its benevolent embrace across iOS, macOS, watchOS, tvOS, and many other platforms, fostering an ecosystem of harmonious coexistence.

Dyld vs. Dyld3: dynamic linker in iOS

In the vast landscape of iOS development, two linkers shine brightly: Dyld and its evolutionary offspring, Dyld3. These dynamic linkers perform the critical role of managing the loading and linking of libraries during an app's launch. For expert iOS developers, a deep understanding of their inner workings is essential.

Dyld

  1. Startup performance: Dyld, a stalwart of iOS, has been meticulously designed to deliver efficient startup performance. It employs an arsenal of optimisations to minimise the time required for loading and linking libraries when an app takes its first breath. This efficiency is paramount in ensuring a seamless user experience.
  2. Lazy binding: Dyld employs the ingenious strategy of lazy binding, which defers symbol resolution until the precise moment when a symbol is first utilized. This mechanism trims the startup overhead by avoiding unnecessary work during the initial stages of the app launch.
  3. Shared caches: In its quest to enhance startup performance, Dyld harnesses the power of shared caches. These caches store pre-processed libraries, allowing multiple applications to share them. This shared resource optimises resource utilisation and further expedites the launch process.

Dyld3: The evolutionary leap

As iOS and macOS continued to evolve, the demands on dynamic linking also grew. This prompted the emergence of Dyld3, representing a substantial leap forward in dynamic linking technology. Dyld3 introduced several key advancements aimed at optimising app performance and resource management.

Key advancements in Dyld3:

  1. Reduced memory overhead: Dyld3 was engineered with a focus on minimising memory overhead. It employs a more efficient data structure for managing loaded libraries, precious in resource-constrained environments such as mobile devices.
  2. Parallel loading: Dyld3 introduces the paradigm of parallel loading, enabling it to load multiple libraries concurrently. This parallelism takes full advantage of multi-core processors, resulting in faster app launch times.
  3. On-demand loading: Dyld3 adopts the strategy of on-demand loading, loading only the portions of libraries that are required at runtime. This "just-in-time" approach conserves memory and accelerates startup times.
  4. Improved symbol binding: Enhancing symbol binding performance, ensuring that symbols are resolved efficiently as an app runs. This is crucial for maintaining smooth app performance during execution.

Creating and distributing an iOS SDK demands careful consideration of various elements. To embark on this journey, you must make critical decisions regarding the type of library (static or dynamic) that best suits your needs.

Dependency management tools like CocoaPods and Swift Package Manager (SPM) are crucial in linking and integrating your SDK into other projects. Understanding library formats, such as frameworks and xcframeworks, is essential for packaging your code effectively. Don't forget the importance of checksums in ensuring the security of your SDK.

Additionally, exploring the advantages of mergeable libraries can help reduce app size and simplify maintenance. Lastly, delve into the world of dynamic linkers like Dyld and Dyld3 to optimise app startup performance and memory usage. By mastering these components, you'll be well-prepared to create and distribute iOS SDKs that enhance the efficiency and reliability of your development projects.

If you haven't heard about Dyte yet, head over to dyte.io to learn how we are revolutionizing communication through our SDKs and libraries and how you can get started quickly on your 10,000 free minutes, which renew every month. You can reach us at support@dyte.io or ask our developer community if you have any questions.