tezvyn:

Swift Library Evolution: Shipping Break-Proof Frameworks

AI-drafted, machine-checkedSource: github.comadvanced

Library Evolution lets you update a Swift framework without breaking apps that use it, even if they don't recompile. It's crucial for shipping binary SDKs. The footgun is that many seemingly safe changes, like adding a struct property, are binary-breaking by…

WHY IT EXISTS Before Swift 5, changing a public type in a compiled framework (like adding a property to a struct) was a breaking change. Every app using that framework had to recompile with the new version to avoid crashes. Library Evolution was created to allow frameworks to evolve independently of the apps that use them, eliminating this fragile dependency.

THE MENTAL MODEL Think of your compiled framework as a service with a public contract, its Application Binary Interface (ABI). Library Evolution defines the rules for changing the implementation behind that contract without breaking existing clients. It's about promising a certain layout and behavior, and using Swift features to either preserve flexibility for future changes or lock down the contract for performance.

HOW IT WORKS By default, Swift enables "resilience" when Library Evolution is on. This means the compiler adds a layer of indirection for accessing things like struct properties or enum cases. This indirection allows the library to change its internal memory layout without affecting the client binary, which looks up the layout at runtime. For performance, you can trade this flexibility away by marking types as @frozen. A frozen type's layout is fixed and becomes part of the public ABI, so it cannot be changed later without breaking clients. Similarly, @inlinable functions copy their code into the client, locking their implementation permanently.

WHEN TO USE IT You must enable Library Evolution mode when compiling any framework you intend to distribute as a binary (.framework or .xcframework) to third parties. It is the standard for SDKs, system frameworks, and any shared library where you don't control all the clients and their recompilation schedules.

WHEN NOT TO USE IT You don't need it if you control all the targets that link against your library, such as in a monolithic app with multiple internal modules. In that case, the entire app is compiled together, so breaking binary compatibility between modules isn't an issue. Enabling it adds a small performance overhead due to the runtime indirections.

ONE CANONICAL EXAMPLE A library ships a public struct: public struct UserInfo { public var id: String }. An app is built using this. Later, the library author adds a new property: public struct UserInfo { public var id: String; public var name: String }. Without library evolution, the old app binary, which expects the struct to have a certain size, would crash. With library evolution, Swift uses runtime metadata to access properties by name, not by a fixed offset. The old app continues to work with the new library; it just can't see or use the new name property.

Read the original → github.com

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.