DEV Community

Cover image for Creating a Swift Framework - The Practical Story
Erez Hod
Erez Hod

Posted on

Creating a Swift Framework - The Practical Story

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:

  1. Modularity: Think of it as LEGO for your code. You can break your project into smaller, manageable pieces, making everything tidy and efficient.
  2. Encapsulation: Frameworks are like superheroes hiding their secret identities. They only show you what you need, keeping the messy stuff behind the scenes.
  3. 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!
  4. 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.
  5. Sharing Is Caring: You can share frameworks with other developers or your coding squad. It's like sharing your favorite playlist, but for code.
  6. 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.

Image Creating a new Xcode Framework project 1

This brings us to the Framework project creation wizard. Fill in the required information and click Next.

Image Creating a new Xcode Framework project 2

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:

  1. {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>.
  2. {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.
  3. 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.

Image Framework project overview

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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)"
        }
    }
} 
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

By the end of this stage, your project structure should look similar to this:

Image Framework project structure in Xcode

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()
}
Enter fullscreen mode Exit fullscreen mode

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`.
    }
}
Enter fullscreen mode Exit fullscreen mode

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

Image Creating an XCWorkspace 1

Name your workspace and save it next to both of our Framework and iOS app projects like so:

Image Creating an XCWorkspace 2

Now, open your {name}.xcworkspace file and at the bottom left, click on the + sign and click on “Add files to {name}”

Image Creating an XCWorkspace 3

Add both *.xcodeproj project files to this workspace. Your workspace should now look similar to this:

Image Creating an XCWorkspace 4

ℹ️ 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:

Image Connecting it all together

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)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Image App example video

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:

  1. 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.
  2. xcodebuild - This is the Xcode CLI command we are going to use via Terminal to create everything
  3. .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.
  4. .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.
  5. .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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡 Some settings definitions:

  1. 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.
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And there we have it folks, our own compiled, built and ready to go Framework to be distributed anywhere we want!

Image Project  folder structure in Finder

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
Enter fullscreen mode Exit fullscreen mode

Save the file and navigate to its location via the Terminal.

Change it’s permissions:

chmod 755 build_framework.sh
Enter fullscreen mode Exit fullscreen mode

And then, run it like so:

./build_framework.sh
Enter fullscreen mode Exit fullscreen mode

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!

Erez Hod.

Top comments (7)

Collapse
 
julio_nietosantiago_23d2 profile image
Julio Nieto Santiago

Hi Erez,

Really nice post!

I have some problems with something similar. I developed a framework, but in my code, I have some dependencies (Alamofire, SwiftyRSA...) and I can't find a way to make my xcframework work when they use dependencies. Xcode tells me that errors with missing symbols. Could you give a hand? I'm desperate.

Thank you in advance!

Collapse
 
erezhod profile image
Erez Hod

Hi, Julio! Great question.

Are you trying to implement these dependencies as an SPM dependencies inside the Framework's project?

I was able to successfully add Alamofire as an SPM dependency and build the archives for both iOS devices and the iOS simulator with no issues.

Collapse
 
julio_nietosantiago_23d2 profile image
Julio Nieto Santiago

Any update?

Thank you and I'm sorry for the insistence

Collapse
 
julio_nietosantiago_23d2 profile image
Julio Nieto Santiago

Thank you for your quick reply!

Could you give me a quick project example? Or a good tutorial. Because I need for my job, and I should have done it a week ago...

Thread Thread
 
erezhod profile image
Erez Hod

Message me on twitter.com/erezhod 🙂

Thread Thread
 
julio_nietosantiago_23d2 profile image
Julio Nieto Santiago

I just tweet you, because you don't have the DM open 😅. My user is twitter.com/tekieiya

Collapse
 
victorjrm profile image
Víctor José Rodriguez_Martin

Hello:

When I try to include the framework in the application it does not appear in the list. What can be?

Greetings from Victor

Hola:

Cuando trato de incluir el framework en la aplicación este no aparece en la lista. ¿Qué puede ser?

Saludos de Víctor