Creating a Swift framework is a fundamental skill for iOS and cross-platform mobile developers who want to build modular, reusable code libraries. Whether you're extracting common functionality from existing projects or building SDKs for third-party integration, Swift frameworks provide the structural foundation for code modularity and distribution.
This guide covers everything from project setup to publishing, with practical examples and best practices that apply to both individual projects and team environments. For teams building comprehensive mobile solutions, understanding framework architecture is essential for maintaining clean codebases that scale efficiently across iOS development projects.
What Is a Swift Framework
A Swift framework is a package of compiled code along with associated resources that can be shared across multiple projects or distributed to other developers. Frameworks encapsulate functionality, making it easy to reuse code, maintain separation of concerns, and manage dependencies efficiently. Unlike traditional static libraries, frameworks include headers, resources, and metadata that enable seamless integration into consuming applications. The framework architecture promotes clean architecture patterns by enforcing clear boundaries between components, which is essential for maintaining large-scale mobile applications.
Dynamic vs Static Frameworks
Swift frameworks come in two primary varieties: dynamic frameworks and static frameworks. Dynamic frameworks are linked at runtime, allowing multiple applications to share a single copy of the framework in memory, which can reduce overall app size when multiple apps use the same framework. This memory efficiency makes dynamic frameworks suitable for scenarios where multiple apps might use the same framework on a user's device.
Static frameworks, by contrast, are linked at compile time and embedded directly into the final binary, which simplifies distribution and avoids runtime loading issues. While static frameworks may result in larger individual app binaries, they eliminate the overhead of dynamic linking at startup and avoid potential compatibility issues with framework versioning. Apple's official documentation notes that static frameworks offer advantages for distribution scenarios where dynamic linking overhead isn't desirable.
Framework Bundle Structure
The framework bundle structure follows a standardized format essential for debugging integration issues and properly configuring build settings when creating distribution-ready frameworks. A framework bundle contains the compiled Swift modules with the .swiftmodule directory storing compiled interface files, header files in the Headers directory for Objective-C interoperability, and resource directories including Resources for assets like images, storyboards, and localized strings.
Understanding this structure helps when debugging integration problems or configuring build phases. The Modules directory contains the module map file that Swift uses to resolve type information, while the Headers directory provides the public interface for Objective-C consumers. Modern Swift development increasingly favors the Swift Package Manager for dependency management, providing a standardized way to define, version, and distribute frameworks without requiring manual build configuration.
Setting Up Your Xcode Project for Framework Development
Creating a framework in Xcode begins with selecting the appropriate project template and configuring the build settings to produce the desired output format.
Step-by-Step Project Creation
- Navigate to File → New → Project in Xcode's menu bar
- Select iOS → Framework & Library from the template browser
- Choose Cocoa Touch Framework for a standard dynamic framework or Cocoa Touch Static Library if you need static linking behavior
- Click Next and configure your product name, organization identifier, and language (Swift)
- Select the deployment targets your framework needs to support
- Check Include Unit Tests to establish a quality gate from the outset
As Thoughtbot's framework guide emphasizes, including unit tests from the outset ensures your framework remains maintainable as it grows.
Scheme Sharing Configuration
After creating the project, you'll need to configure scheme sharing if you plan to distribute your framework through Carthage or integrate it into workspaces. Access the scheme menu in Xcode's toolbar (next to the Run/Stop buttons), select Manage Schemes, and ensure your framework scheme is marked as Shared. This setting generates the Scheme.plist file that build tools need to identify and build your framework targets automatically.
Without shared schemes, automated build systems cannot reliably produce your framework, making this a critical configuration for any distribution scenario. Commit the shared scheme file to version control so team members and CI systems can build your framework consistently.
Build Settings Configuration
The build settings within your framework project require careful attention to several key areas:
-
Mach-O Type: Determines whether Xcode produces a dynamic or static framework. Set to
Frameworkfor dynamic linking orStatic Libraryfor static builds. -
Application Extension API Only: Restricts your framework to APIs available in app extensions. Set to
YESwhen your framework targets extension use cases. -
Skip Install: Should typically be set to
YESfor framework targets to prevent Xcode from treating the framework as an installable product. -
Define Module: Set to
YESto generate the module map needed for Swift interoperability.
According to Apple's documentation on static frameworks, these settings control how your compiled code is packaged and linked by consuming applications.
Swift Package Manager Framework Creation
Swift Package Manager has become Apple's recommended approach for managing Swift dependencies, offering deep integration with Xcode and the Swift build system. Unlike third-party solutions like CocoaPods and Carthage, SPM is a first-party tool that ships with Xcode, eliminating the need for additional installation or configuration.
Creating a Package
You can create a Swift Package using Xcode's GUI or the terminal:
Using Xcode:
- Select File → New → Package
- Choose a location and name for your package
- Xcode generates the basic project structure including
Package.swift
Using Terminal:
mkdir MyFramework
cd MyFramework
swift package init
As SwiftLee's SPM guide notes, this creates the essential project structure automatically.
Package.swift Configuration
The Package.swift file defines your framework's identity, products, dependencies, and targets:
-
Tools version: The
// swift-tools-version:5.10comment specifies the minimum Swift version required, determining which SPM features are available -
Platform configuration: Declare supported operating system versions like
.iOS(.v15),.macOS(.v12)to enable newer Swift features while communicating compatibility requirements -
Products definition: Export your framework to consumers using
.library(name:name:targets:)for dynamic products -
Dependencies array: Specify external packages with version requirements using semantic versioning
-
Targets configuration: Define your framework's modules and test bundles, including resource processing rules
For teams working on web development services, Swift Package Manager offers a unified approach to dependency management that aligns well with modern development workflows across platforms.
1// swift-tools-version:5.102import PackageDescription3 4let package = Package(5 name: "YourFramework",6 platforms: [7 .iOS(.v15),8 .macOS(.v12),9 .watchOS(.v8),10 .tvOS(.v15)11 ],12 products: [13 .library(14 name: "YourFramework",15 targets: ["YourFramework"]16 )17 ],18 dependencies: [19 .package(url: "https://github.com/example/dependency.git", from: "1.0.0")20 ],21 targets: [22 .target(23 name: "YourFramework",24 dependencies: [],25 resources: [.process("Resources")]26 ),27 .testTarget(28 name: "YourFrameworkTests",29 dependencies: ["YourFramework"]30 )31 ]32)Managing Dependencies in Swift Frameworks
Dependency management is a critical aspect of framework development, enabling you to leverage existing code while maintaining control over your project's build configuration.
Dependency Specification Options
Swift Package Manager provides several mechanisms for specifying dependency requirements:
-
Semantic versioning:
.package(url: from: "1.0.0")allows patch and minor version updates while preventing major version changes that might introduce breaking changes -
Exact version:
.package(url: exact: "1.2.0")locks your package to a specific version regardless of newer releases, useful after thorough testing -
Branch-based:
.package(url: branch: "develop")integrates with unreleased changes in dependency repositories during development -
Revision-based:
.package(url: revision: "abc123def")pins to a specific commit hash for maximum control
As the SwiftLee SPM guide explains, the most common approach uses semantic versioning for balanced flexibility and stability.
Linking Dependencies to Targets
When adding dependencies to your framework, you must declare them in the Package.swift dependencies array and then reference them in target configurations:
targets: [
.target(
name: "YourFramework",
dependencies: [
".product(
name: "DependencyPackage",
package: "dependency-package
)"]
),
.testTarget(
name: "YourFrameworkTests",
dependencies: ["YourFramework"]
)
]
Runtime dependencies become part of your framework's dependency graph, while test-only dependencies are excluded from consumers' builds. This distinction reduces final app size by preventing unnecessary transitive dependencies from being embedded.
Designing Public APIs for Framework Consumers
The public API of your framework determines how consumers interact with your code and should be designed with clarity, consistency, and future evolution in mind.
Access Control in Swift Frameworks
Swift's access control modifiers provide the tools for defining your API surface:
-
public: Exposes types and members to consumers while keeping implementation details hidden within your framework module
-
open: Reserved for classes and class members that consumers can subclass or override--use sparingly to preserve your ability to evolve the implementation
-
internal: Default visibility, hidden from consumers but accessible across files within your framework
-
fileprivate/private: Implementation details completely hidden at the file or declaration level
As Thoughtbot's framework guide notes, the open modifier should be used sparingly.
API Design Best Practices
A common pitfall is exposing implementation details as public APIs, creating unnecessary coupling between consumers and your internal design:
Poor API Design:
public class NetworkManager {
public var session: URLSession // Exposing internal detail
public func configure(with config: Config) // Implementation exposed
}
Good API Design:
public class NetworkManager {
public init(baseURL: URL) // Clean, focused API
public func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}
Start by defining what functionality you want to expose, then carefully annotate only those declarations with public access. Test your public API without @testable import to validate that consumers can use your framework correctly. This practice catches cases where you've accidentally relied on internal access.
For developers working with data serialization in their frameworks, understanding how to properly design APIs is crucial. See our guide on Simplify JSON Parsing Swift Using Codable for patterns that integrate seamlessly with well-designed framework APIs.
Bundling Resources with Swift Frameworks
Swift frameworks can include resources such as images, asset catalogs, localized strings, and other assets that accompany your compiled code.
Resource Processing Strategies
SPM targets define resources using the resources array within target configurations:
-
.process(): Automatically handles common resource types including asset catalog optimization and localization file processing
-
.copy(): Includes resources without modification, appropriate for files that should remain unchanged
-
.exclude(): Prevents specific files from being included in the final bundle
Accessing Bundled Resources
Accessing bundled resources from your framework code requires using the Bundle.module property, which provides access to the resource bundle associated with your framework module:
import UIKit
public struct ImageLoader {
public static func loadImage(named name: String) -> UIImage? {
return UIImage(named: name, in: .module, compatibleWith: nil)
}
public static func loadTextFile(named name: String) -> String? {
guard let url = Bundle.module.url(forResource: name, withExtension: "txt") else {
return nil
}
return try? String(contentsOf: url, encoding: .utf8)
}
}
According to SwiftLee's resource handling guide, note that SwiftUI's Image initializer with bundle parameter doesn't work directly within framework code due to framework bundle loading semantics. The workaround involves using UIKit's image loading API and bridging to SwiftUI where needed, adding minor complexity but producing correct behavior across all supported platforms.
Testing Your Swift Framework
Comprehensive testing is essential for frameworks that will be used by multiple projects or distributed to other developers.
Testing Approach
- Use XCTest as the foundation for unit testing
- Consider Quick/Nimble for BDD-style testing syntax that improves readability
- Include test targets from the beginning of framework development
- Test public API without
@testableimport to validate API completeness
As Thoughtbot recommends, establish a quality gate that prevents regressions as the framework grows.
Example Test Structure
import XCTest
@testable import YourFramework // Use @testable for internal testing
final class YourFrameworkTests: XCTestCase {
func testPublicAPI() {
// Test your framework's public API
let result = YourFramework.doSomething()
XCTAssertNotNil(result)
}
func testEdgeCases() {
// Test boundary conditions
}
}
Running Tests
- Xcode: Use the test navigator (⌘+6) or ⌘U keyboard shortcut
- Command line:
swift testfor terminal execution - CI integration: Automated test execution in your pipeline
The SwiftLee SPM guide notes that swift test integrates well with CI systems that may not have Xcode's full UI testing capabilities.
Distributing Your Swift Framework
Distribution involves choosing the appropriate mechanism based on your goals and consumer needs.
Swift Package Manager (Recommended)
SPM is the modern, Apple-recommended approach for most distribution scenarios:
- Host your framework's repository on a Git server (GitHub, GitLab, etc.)
- Create version tags:
git tag 1.0.0 && git push origin 1.0.0 - Add your package to the Swift Package Index for discoverability
Consumers add your framework with Xcode's Add Package Dependency feature or by adding to their Package.swift.
CocoaPods Distribution
For teams using CocoaPods as their primary dependency manager:
- Create a podspec file with your framework's metadata, source locations, and dependency requirements
- Validate locally:
pod lib lint - Publish to the trunk:
pod trunk push
As Thoughtbot notes, validation ensures your podspec meets CocoaPods specifications before distribution.
Carthage Distribution
Carthage requires shared schemes and produces prebuilt binaries:
- Ensure your Xcode scheme is shared (Manage Schemes → Shared)
- Build the framework:
carthage build --archive - Create a GitHub release and attach the built framework archive
- Consumers add
github "YourOrg/YourFramework"to their Cartfile
The --use-submodules flag includes your source repository as a submodule rather than downloading a prebuilt binary, useful for development workflows.
For teams implementing AI-powered mobile solutions, distributing frameworks through multiple channels ensures your utilities reach all consumers regardless of their preferred dependency management approach. Learn more about AI automation services for integrating intelligent features into mobile applications.
Best Practices for Swift Framework Development
Following established best practices ensures your framework is maintainable, usable, and professional quality.
Versioning
Use semantic versioning principles:
- Patch (1.0.0 → 1.0.1): Bug fixes that don't change behavior
- Minor (1.0.0 → 1.1.0): New features that maintain backward compatibility
- Major (1.0.0 → 2.0.0): Breaking changes that require consumer updates
Additional Best Practices
- Documentation: Write documentation comments on all public APIs using Markdown format
- Focus: Keep frameworks focused on specific functionality rather than attempting to provide every possible feature
- Minimal dependencies: Minimize external dependencies to reduce integration complexity
- Examples: Include example projects or playgrounds that demonstrate usage
- Changelog: Maintain a changelog for consumers to understand what changed between versions
Well-designed frameworks separate concerns clearly, making them easier to maintain, understand, and integrate. If your framework grows too large, consider splitting it into multiple, related frameworks that can be integrated independently.
Common Questions About Swift Frameworks
Swift Extensions: An Overview With Examples
Learn about Swift extensions for extending framework capabilities
Learn moreSimplify JSON Parsing Swift Using Codable
Data serialization patterns for framework development
Learn moreUnderstanding Kotlin Generics
Cross-platform generic programming concepts
Learn moreSources
- Thoughtbot: Creating your first iOS Framework - Comprehensive guide covering Xcode project setup, Carthage integration, CocoaPods support, and testing frameworks
- SwiftLee: Swift Package Manager framework creation in Xcode - In-depth coverage of SPM-based framework creation, Package.swift configuration, platform requirements, and resource bundling
- Apple Developer Documentation: Creating a static framework - Official documentation on static framework configuration in Xcode