Creating a Swift Framework - The Practical Story
Erez Hod
Posted on November 2, 2023
The code in this article was written using Xcode 15.0.1 & Swift 5.9
What are Swift Frameworks?
In the world of Swift, a framework is like a toolbox on steroids for building awesome apps. It's a cool collection of code and goodies that you can use and reuse to make your life as a developer way easier.
So, what's a Swift framework, you ask? Well, it's like this organized, modular, and encapsulated chunk of code and other stuff, like images or text, that you can drop into your projects.
Here's why they're super cool:
- Modularity: Think of it as LEGO for your code. You can break your project into smaller, manageable pieces, making everything tidy and efficient.
- Encapsulation: Frameworks are like superheroes hiding their secret identities. They only show you what you need, keeping the messy stuff behind the scenes.
- Code Reusability: Ever wanted to reuse a piece of code in multiple projects? With frameworks, it's as easy as borrowing your buddy's jacket for the weekend. No more copy-pasting!
- Dependency Magic: Frameworks can rely on other frameworks. It's like calling in your friends for help when things get tough. You don't have to reinvent the wheel.
- Sharing Is Caring: You can share frameworks with other developers or your coding squad. It's like sharing your favorite playlist, but for code.
- Apple-Friendly: These bad boys work across Apple's playgrounds: iOS, macOS, watchOS, and tvOS. So you can build cool stuff everywhere.
So there you have it – Swift frameworks, making coding life more fun, efficient, and social, no matter which Apple platform you're rocking.
What are we going to build?
We're gonna whip up a killer framework to make life a breeze for our users, all thanks to the totally free Bored API.
Once we've got our framework done, we will implement it in a demo iOS app using some basic SwiftUI and Swift Concurrency (async/await).
Creating your first framework
Creating your first framework in Xcode is as easy as creating a new project.
Open Xcode and click on “Create New Project…”.
From there, choose a “Framework” type project, under the Framework & Library section and click Next.
This brings us to the Framework project creation wizard. Fill in the required information and click Next.
There you go, you have created your own Framework project. Easy peasy.
Framework project overview
Our Framework project comes with a few starter files and settings:
-
{FrameworkName}.h - This is an umbrella header. You can use this header file to import all public headers of your framework using statements like
#import <BoredFramework/PublicHeader.h>
. - {FrameworkName}.docc - Will be created if you checked the “Include Documentation” option in the new project wizard. This DocC folder contains a single documentation file in markdown format to start writing your documentation. This can be exported later as a DocC Xcode documentation.
- Unit Test Folder - If you have checked the “Include Tests” option in the new project wizard.
By default, this project comes with basic destination availability that you can change as you see fit. If you want your Framework to support iOS only, you can remove all other options and leave iOS.
Adding some code
Now its time to put some code inside our shiny new Framework.
For this stage, I have pre-written a few files that contain Swift code to add some business logic and achieve our goal, make the Bored API more accessible to our developer fellows.
ℹ️ NOTE: You can copy this code to your new Framework project, or write something completely custom on your own. It should not affect the progress for the rest of this tutorial
🛑 ATTENTION: By default, all entities you create are defined as internal
, meaning they can be accessed only from within the Framework project.
Any entity that we want to expose to our fellow developers who implement this Framework, should be marked with the public
access control keyword.
BoredFramework.swift
- This will be our entry file/class to make our Framework APIs accessible to our developers
// Enforce the minimum Swift version for all platforms and build systems.
// Note that you can use whichever version you like, or not implement this at all.
#if swift(<5.9)
#error("BoredFramework doesn't support Swift versions below 5.9.")
#endif
/// Reference to `BoredFramework.default` for quick bootstrapping; Alamofire style!
public let Bored = BoredFramework.default
public class BoredFramework {
/// Shared singleton instance.
public static let `default` = BoredFramework()
// Prevent developers from creating their own instances by making the initializer `private`.
private init() {}
}
// MARK: - Public developer APIs
public extension BoredFramework {
/**
Fetch an `Activity` from Bored API.
This is our API method for external developers who are going to utilize our framework.
*/
func fetchActivity() async -> Result<Activity, BoredFrameworkError> {
guard let url = URL(string: "https://www.boredapi.com/api/activity") else {
return .failure(.invalidURL)
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let activity = try JSONDecoder().decode(Activity.self, from: data)
return .success(activity)
} catch DecodingError.dataCorrupted(let error) {
return .failure(.decodingError(error))
} catch let error {
return .failure(.requestError(error))
}
}
}
BoredFrameworkError.swift
- This contains a basic error enum
with basic error cases
public enum BoredFrameworkError: Error {
case invalidURL
case requestError(_ error: Error)
case decodingError(_ error: DecodingError.Context)
public var localizedDescription: String {
switch self {
case .invalidURL: return "Invalid URL"
case .requestError(let error): return "Request error: \(error.localizedDescription)"
case .decodingError(let error): return "Decoding error: \(error.debugDescription)"
}
}
}
Activity.swift
- This is the model struct file we are implemented in coherence with the Bored API response
public struct Activity: Codable {
public let activity: String
public let type: String
public let participants: Int
public let price: Double
public let link: String
public let key: String
public let accessibility: Double
}
By the end of this stage, your project structure should look similar to this:
Testing your new Framework
You are probably asking yourselves right now…
Boss, how can I check that my code actually works? There’s no option to run it on a simulator or… anything for that matter. — You
While you can build your Framework project for any target and make sure that it actually compiles without errors, you certainly cannot run it on a simulator or device since it’s not an app project.
In order to test that our Framework’s code implementation actually works, we’ll need to create a demo app that will use our new Framework - just like other developers are going to do.
To achieve this, we are going to create an iOS SwiftUI project and an Xcode workspace file that will contain everything together.
Creating a new iOS SwiftUI app
I’m not going to go too deep into creating this type of project since its something most of us should already know how to do, so I’m just going to output here all of the code I have written for this demo app
ContentView.swift
- Our main screen written in SwiftUI. This file comes by default with every new iOS SwiftUI project
struct ContentView: View {
@ObservedObject private var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("🥱")
.font(.system(size: 80.0))
Text("Are You Bored?")
.font(.title)
Text(viewModel.activityDescription)
.padding()
Button("Generate Activity") {
viewModel.generateActivity()
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.isLoading)
}
.padding()
}
}
#Preview {
ContentView()
}
ContentViewModel.swift
- The ViewModel file for our main screen. This class will implement our Framework.
final class ContentViewModel: ObservableObject {
@Published var isLoading = false
@Published var activityDescription = "Tap 👇 to generate an activity"
func generateActivity() {
// TODO: Fetch a new activity from the `BoredFramework`.
}
}
That’s it! We have our iOS SwiftUI app set-up with some basic UI and we can start merging it all together.
Creating an XCWorkspace
In order to merge it all together, we’ll create an XCWorkspace file, which you probably know from using CocoaPods, or simply because you had to combine a few projects together at some point.
In order to create a new XCWorkspace, in Xcode, click on File > New > Workspace
Name your workspace and save it next to both of our Framework and iOS app projects like so:
Now, open your {name}.xcworkspace file and at the bottom left, click on the + sign and click on “Add files to {name}”
Add both *.xcodeproj
project files to this workspace. Your workspace should now look similar to this:
ℹ️ NOTE: You will now be able to see both Framework and App targets in the target/destination selector at the top-center of the Xcode window. Choose the App to compile and run the app and Framework targets to check that everything runs smoothly.
Connecting it all together
Let’s connect our App and Framework together.
To achieve this, we will make our App consume the Framework by going to our App’s project settings, then, under the Frameworks, Libraries, and Embedded Content section, click on the + sign and add our new Framework to the app project as a dependency like so:
Now we can import our Framework and use it! Go to the ContentViewModel.swift
file in the iOS app and replace the code implementation to this:
import Foundation
import BoredFramework // <-- importing the BoredFramework
final class ContentViewModel: ObservableObject {
@Published var isLoading = false
@Published var activityDescription = "Tap 👇 to generate an activity"
func generateActivity() {
isLoading = true
Task { @MainActor [weak self] in
guard let self else { return }
let result = await Bored.fetchActivity() // <-- Implementing the BoredFramework in our app
isLoading = false
switch result {
case .success(let activity):
activityDescription = "You could \(activity.activity.lowercased())"
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
Take note in how we can now import our Framework, in this case the BoredFramework
and use its exposed API fetchActivity()
.
It all works!
Compile and run your iOS app in the simulator or on a device and watch how it lives in its mighty glory!
We can now generate new activities for bored users who are looking for nice activities to do, all fetched from our modular new Framework that connects with the free Bored API.
Deploying the Framework
Before deploying our new Framework so others can use it, we need to go through some basic terms and tools in order to understand what we’re about to see and do here:
- Platform destination - When creating a Framework for deployment, we can only create it with support for 1 destination and architecture (device/simulator, arm64/x86, iOS/macOS/watchOS/tvOS). Therefore, we are going to be making 2
.framework
files and combine them using an.xcframework
for deployment on more than 1 architecture/platform. -
xcodebuild
- This is the Xcode CLI command we are going to use via Terminal to create everything -
.xcarchive
- Before creating a.fraemwork
file, we must compile our source code into an.xcarchive
, just like when deploying an app to the App Store. -
.framework
- This will be the framework file itself. It contains the Binary file of the compiled code and some helper files that we are not going to go through in this tutorial. -
.xcframework
- When creating a Framework for multiple platforms (e.g.: iOS Simulator + iOS Device), we will be combining those frameworks into 1.xcframework
that can be used by Xcode. This way, we can use a single file to support multiple platforms and architectures.
Creating the Framework using the Xcode CLI
Now we are ready to start creating the actual Framework that can be sent to other developers and be used by them.
Open your favorite Terminal client and navigate to the project’s folder on your disk where the .xcworkspace
file is.
From there, we’ll start by cleaning the workspace’s cache using the xcodebuild
command, so we can start from a clean slate:
xcodebuild \
clean \ # Using the `clean` command
-workspace Bored.xcworkspace \ # The name of your .xcworkspace file
-scheme BoredFramework # The scheme target we want to clean. In this case it's our Framework target name
Next, let’s create our first .xcarchive
inside a new folder called build
:
# Create an archive of the Framework for iOS devices
xcodebuild \
archive \ # Using the `archive` command
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
-workspace Bored.xcworkspace \ # The name of your .xcworkspace file
-scheme BoredFramework \ # The scheme target we want to clean. In this case it's our Framework target name
-configuration Release \ # Release configuration (not debug)
-destination "generic/platform=iOS" \ # The platform. In this case, we are targeting iOS devices architecture
-archivePath build/BoredFramework-iOS.xcarchive # The path for the newly created .xcarchive file. It will be created inside a build folder
💡 Some settings definitions:
-
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
- This setting is used when you want to build your framework for distribution, meaning that you intend to share it with others or distribute it through platforms like the App Store or a package manager.
- When you set this to
YES
, Xcode ensures that the framework is built with the necessary settings and optimizations for distribution. - It may enable certain features like bitcode generation, which is important for app thinning and optimization during app distribution.
-
SKIP_INSTALL=NO
- This setting controls whether the built framework should be copied to the installation directory or not.
- When set to
NO
, Xcode will copy the framework to the designated installation directory when you build your project. - This is typically used when you want the framework to be part of your project's build products, and you intend to use it within the same project or share it with other projects.
Another one for iOS simulators architectures:
# Create an archive of the Framework for iOS simulators
xcodebuild \
archive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
-workspace Bored.xcworkspace \
-scheme BoredFramework \
-configuration Release \
-destination "generic/platform=iOS Simulator" \ # The platform. In this case, we are targeting the iOS simulator architecture
-archivePath build/BoredFramework-iOS_Simulator.xcarchive # Note that we are using a similar name, but appending "_Simulator" at the end for distinction
Now, we are ready to combine them both into an .xcframework
:
# Convert the archives to .framework
# and package them both into one .xcframework
# then, remove the build folder
xcodebuild \
-create-xcframework \
-archive build/BoredFramework-iOS.xcarchive -framework Bored.framework \
-archive build/BoredFramework-iOS_Simulator.xcarchive -framework Bored.framework \
-output output/BoredFramework.xcframework &&\
rm -rf build # Remove our unneccessary build folder with the xcarchive files
And there we have it folks, our own compiled, built and ready to go Framework to be distributed anywhere we want!
You can drag this .xcfamework
into an Xcode project and start using it right away.
ℹ️ NOTE: Since this is a compiled framework, its implementation will be hidden as it's entirely binary. Only the public headers are exposed
Pro tip: Creating your own automated shell script
You can create your own shell script file that can be executed via the Terminal without inputting all of the command above every time.
Create an empty text file in the project’s root (next to the .xcworkspace
file) and call it build_framework.sh
, of course you can decided on whichever other name you want.
Open this file in a text editor and put in this whole script:
# A shell script for creating an XCFramework for iOS.
# Starting from a clean slate
# Removing the build and output folders
rm -rf ./build &&\
rm -rf ./output &&\
# Cleaning the workspace cache
xcodebuild \
clean \
-workspace Bored.xcworkspace \
-scheme BoredFramework
# Create an archive for iOS devices
xcodebuild \
archive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
-workspace Bored.xcworkspace \
-scheme BoredFramework \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath build/BoredFramework-iOS.xcarchive
# Create an archive for iOS simulators
xcodebuild \
archive \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
-workspace Bored.xcworkspace \
-scheme BoredFramework \
-configuration Release \
-destination "generic/platform=iOS Simulator" \
-archivePath build/BoredFramework-iOS_Simulator.xcarchive
# Convert the archives to .framework
# and package them both into one xcframework
xcodebuild \
-create-xcframework \
-archive build/BoredFramework-iOS.xcarchive -framework BoredFramework.framework \
-archive build/BoredFramework-iOS_Simulator.xcarchive -framework BoredFramework.framework \
-output output/BoredFramework.xcframework &&\
rm -rf build
Save the file and navigate to its location via the Terminal.
Change it’s permissions:
chmod 755 build_framework.sh
And then, run it like so:
./build_framework.sh
This should run everything and output an .xcframework
file ready to go.
Summary
Swift Frameworks are a banger when used correctly. They can be used to decouple source code from your app, increasing its modularity, reusability, share-ability and even increase its performance in some cases where the Framework is coming pre-compiled.
We have learned how to create our own Framework project, combine it with a demo app for testing, dove deeper into how Swift Frameworks work, building and compiling it into an XCFramework ready to be delivered to other developers and even built our own automated shell script to build our Framework.
The full source code for this project can be found on GitHub.
👨🏻💻 Happy coding!
Posted on November 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.