This article is my explanation about Library Evolution Support and why does it matter.
Before dive into the main topic, let's start with a few terms that might confuse us: ABI Stability and Module Stability
Let first find out what is ABI:
In computer software, an application binary interface (ABI) is an interface between two binary program modules -- Wikipedia
It sounds similar to Application Programming Interface (API). But while API provides an abstract interface at a high-level, hardware-independent, often in human-readable format, ABI provides an interface at a lower level, hardware-dependent, including implementation details about the program.
Before Swift 5.0, an app written in Swift will be bundled with Swift standard libraries (a bunch of libSwift*.dylib files). And an app can run on any past, present, and future OS releases.
Then Swift 5.0 released with ABI Stability
ABI stability means locking down the ABI to the point that future compiler versions can produce binaries conforming to the stable ABI. ABI stability enables binary compatibility between applications and libraries compiled with different Swift versions. -- Swift ABI Stability Manifesto
As a result, Swift runtime is now a part of the Operating System, rather than embedded into the app
Advantages of ABI stability:
In short, it's all about how an application talks to Swift libraries at runtime through an ABI and how important it is to have ABI stability.
Next, we will talk about Module Stability.
Module compiled with Swift 5.0.1 cannot be imported by the Swift 5.1 compiler
You will see this error when importing a binary framework built with an old version of the Swift compiler.
To resolve this, from Swift 5.1, you can enable Module stability by turning on a new flag. Then you can distribute your pre-built framework without worrying about compiler version incompatible.
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
With this flag enabled, Swift compiler will generate a new header file with suffix swiftinterface instead of the swiftmodule header file. swiftinterface file is in text-based format & is forward-compatible with any Swift compiler version >= 5.1.
// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
// swift-module-flags: -target arm64-apple-ios13.0 -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name PaymentModule
import Foundation
import NetworkModule
@_exported import PaymentModule
import Swift
@_hasMissingDesignatedInitializers final public class PaymentManager {
public static let shared: PaymentModule.PaymentManager
final public func setup(isDebugMode: Swift.Bool = false)
final public func tellMeAboutNetworkConfigType()
@objc deinit
}
But that's not over, BUILD_LIBRARY_FOR_DISTRIBUTION also enable Library Evolution. Let's find out what it is.
Without Library Evolution, it's very hard for the library to be Binary compatible in each release. It's a common problem called fragile binary interface problem, in which even a tiny internal change will break ABI and require recompiling of everything upstream.
In the first part, we know ABI contains implementation details of the binary program, such as Data Layout, Type Metadata, Name mangling, etc... For example, a memory layout of a struct depends on its properties. So adding a new property, renaming a property, and even changing the order of their declaration, will break the ABI.
In the next part, I will demonstrate this problem with a small demo.
Imagine we are building and maintaining an iOS shopping app. It's a common practice to break down an app into several small modules, each module built into a separated framework.
In the above diagram, there is a PaymentModule which imports NetworkModule to make API call to a payment gateway. At the same time, the shopping app imports the NetworkModule directly to make API calls as need.
All these modules are prebuilt binaries. PaymentModule (v1.0.0) was compiled and linked against NetworkModule (v1.0.0). Library Evolution mode is disabled by default.

Inside NetworkModule, I created a Config struct with a bool isDebugMode for network logging purpose.
public struct Config {
public let isDebugMode: Bool
public init(isDebugMode: Bool) {
self.isDebugMode = isDebugMode
}
}
That's all for NetworkModule. Let's move to the PaymentModule.

Inside PaymentModule, I created a singleton PaymentManager class
import NetworkModule
public final class PaymentManager {
public static let shared = PaymentManager()
public func setup(isDebugMode: Bool = false) {
let networkConfig = Config(isDebugMode: isDebugMode)
print("Debug mode: \(networkConfig.isDebugMode)")
}
public func tellMeAboutNetworkConfigType() {
print(
"""
Size: \(MemoryLayout<Config>.size)
Stride: \(MemoryLayout<Config>.stride)
Alignment: \(MemoryLayout<Config>.alignment)
"""
)
}
}
Method setup accept the boolean param, init the Config struct with that param, then print out the boolean property of Config.
We have another method named tellMeAboutNetworkConfigType which simply prints out the Memory Layout of Config struct. For more information about struct MemoryLayout, this article explained it all: https://swiftunboxed.com/internals/size-stride-alignment/
Next step, we build each module into an XCFramework. Project was archived with Library Evolution disabled
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=NO
Then we use xcodebuild -create-xcframework -allow-internal-distribution to generate an xcframework without Module stability and Library Evolution
Now we can drag those frameworks into the Shopping App.
First, we make a copy of tellMeAboutNetworkConfigType method inside Shopping App
Then in viewDidLoad, we call setup passed isDebugMode as true, and call tellMeAboutNetworkConfigType from PaymentModule and from the Shopping App itself.
private func setup() {
print("----- PAYMENT MODULE -----")
PaymentManager.shared.tellMeAboutNetworkConfigType()
print("--------------------------")
print("------ SHOPPING APP ------")
tellMeAboutNetworkConfigType()
print("--------------------------")
PaymentManager.shared.setup(isDebugMode: true)
}
private func tellMeAboutNetworkConfigType() {
print(
"""
Size: \(MemoryLayout<Config>.size)
Stride: \(MemoryLayout<Config>.stride)
Alignment: \(MemoryLayout<Config>.alignment)
"""
)
}
Let's run and see what console output is:
----- PAYMENT MODULE -----
Size: 1
Stride: 1
Alignment: 1
--------------------------
------ SHOPPING APP ------
Size: 1
Stride: 1
Alignment: 1
--------------------------
Debug mode: true
Everything is normal. The Config struct has its size equal to the size of the isDebugMode boolean, which is 1 byte. We passed isDebugMode as true, so the PaymentModule printed it out as true.
Next is the fun part. We want to add a logging level to the NetworkModule. So we add one Int property with default value set to 0. This minor change is backward compatible because no public interface changed.
public struct Config {
public var logLevel: Int = 0
public let isDebugMode: Bool
public init(isDebugMode: Bool) {
self.isDebugMode = isDebugMode
}
}
We rebuild the NetworkModule, and update our Shopping App with the new framework binary (v1.0.1)
Now we do a clean build and run the Shopping App again. Let's see what happens.
----- PAYMENT MODULE -----
Size: 1
Stride: 1
Alignment: 1
--------------------------
------ SHOPPING APP ------
Size: 9
Stride: 16
Alignment: 8
--------------------------
Debug mode: false
Wait, they are telling different things. And why debug mode is false! 😨
This scenario is an example of Binary incompatible. PaymentModule was linked against the old version of NetworkModule. When linking, it relies on the old ABI of NetworkModule. Later on, we updated the NetworkModule in Shopping App without recompiling PaymentModule. So, when PaymentModule tried to read isDebugMode, it accessed the first byte, which is now the first byte of the logLevel integer (default to zero), representing false bool value.
Now let's turn on Library Evolution.
BUILD_LIBRARY_FOR_DISTRIBUTION & removed the -allow-internal-distribution build param.NetworkModule to v1.0.0 and build PaymentModule.logLevel again and built NetworkModule to the new version v1.0.1.PaymentModule (v1.0.0) and NetworkModule (v1.0.1) into ShoppingApp.This time, PaymentModule, despite of linking against NetworkModule v1.0.0, printed out the correct Memory Layout of Config struct in v1.0.1 of NetworkModule, thanks to Library Evolution.
----- PAYMENT MODULE -----
Size: 9
Stride: 16
Alignment: 8
--------------------------
------ SHOPPING APP ------
Size: 9
Stride: 16
Alignment: 8
--------------------------
Debug mode: true
If you are distributing library as a binary package, then you should be aware of Binary Compatible and turn on Library Evolution & Module Stability. Also, please check out here for more optimization with @frozen keyword.
🍎🍎