Creating an Offline iOS Framework with Swift Package Manager (SPM)

Tomer Glick
Beta Blog
Published in
7 min readMay 7, 2023

--

Creating your own package or framework is not new in principle. You can use open-source code from third parties with the help of packages. Additionally, packages make it simpler to divide your code to modules so it will be easy to understand and manageable, logical chunks that you can readily share across your projects — or even with the whole community.

Swift Package Manager has been around for several years, It was initially restricted to server-side or swift command-line programs. Since Swift 5.0 (and Xcode 11) SwiftPM is compatible with the iOS, macOS, and tvOS build systems for making apps. The fact that app developers can now unleash its power on their codebases is fantastic news!

In this article we will focus on solving a specific problem I encountered at work that will be explained right away. The article is directed for experienced developers that already have knowledge about packages and git, If you came across this article and not familiar with these subjects, I suggest you begin with the basics, here are some articles and videos I recommend to start with:

Swift official docs — https://www.swift.org/package-manager/

Apple WWDC videos about SPM — here and here

About Versioning strategies — https://semver.org/

Git & Bitbucket — https://www.atlassian.com/git/tutorials

Also If you happen to have a Kodeco account I recommend these articles —

If not, it’s ok, there are plenty of great tutorials on YouTube

Ok, So now that we have covered the basics of creating a Swift Package we can move forward to explaining the issue that is the title of this article. I work in a large financial institution, that requires a very secured and restricted working environment, Our CI processes do not have active internet connection so in order to use packages we must load them into our private git (of course while taking in consideration all of the policy requirements of 3rd party frameworks and etc).

In order to demonstrate our issue, we will examine the firebase package, We can see it has multiple dependencies, if we are to use that package there will be a big problem to the CI processes since it wouldn’t be able to load those dependencies from the web. Also we really don’t want to get back to importing those xcframeworks directly to our projects since we have multiple apps and it will be messy and hard to maintain.

Example dependency: The google app measurement dependency linked to the GitHub project:

Internet Dependency

So how the magic is done?

Since the Firebase SDK is complicated enough I will use it to demonstrate. our opening point will be a project I made ahead with Firebase implemented as a regular Swift Package from GitHub. (refer to FirebaseSample1)

Create a new App Project, using swift as language, add the Firebase package by selecting FileAdd Packages… in Xcode’s menu bar, paste the firebase link — https://github.com/firebase/firebase-ios-sdk.git in the search bar, click the add package button, when prompt choose only the FirebaseAnalytics library and click again on the add package button, Your project should look like this —

If you got errors please follow the instructions on this page

Now We want to use the Firebase Analytics library so we know it was implemented correctly by adding the firebase initialization.

Run build, we get build success so all good to go.

Our next step will be to create the offline package so we won’t need to depend on the internet connection, follow the next steps:

  1. Create a new package and import it as local to our project, Xcode allows using local packages, It really helps developing the initial package before we actually place it in our private repository. Select FileNew Package.., name it MyFirebase and click Create.

This is the package template, we will change it soon.

2. Close the Package Xcode project, Get back to our FirebaseSample project, get to Add Package and then click on Add Local.. Navigate to the folder of your new package and click on Add Package.

Now we can work on our package inside the project

3. Download the firebase manual installation zip from this link.

4. Like the online package import, we are going to use only the Analytics library so it is very important to follow the next steps carefully —

  • Delete the content of the “Sources” folder inside our package.
  • Drag all of the xcframeworks from the “FirebaseAnalytics” folder to the “Sources” folder in our package.
  • Create a “Headers” folders under the package root, and drag the “Firebase.h” file inside as well.

The Package should look like this —

There will be also Xcode errors, don’t mind them for now.

  • Now we need to make changes in the package.swift file so our package will know which libraries and headers it contains and how to expose them to the consumer which is our project.
  • For each library we added to our project we will add the library name in the library → products section, and also adding a “.binaryTarget” row pointing to the library (filesystem) → library section
  • Finally We will add a “headerSearchPath”. row so the header file will be exposed.

The final package.swift file will look like this:

import PackageDescription

let package = Package(
name: "MyFirebase",
products: [
.library(
name: "MyFirebase",
targets: ["FirebaseCore",
"FBLPromises",
"FirebaseAnalytics",
"FirebaseAnalyticsSwift",
"FirebaseCoreInternal",
"FirebaseInstallations",
"GoogleAppMeasurement",
"GoogleAppMeasurementIdentitySupport",
"GoogleUtilities",
"nanopb",]),
],
targets: [
.target(
name: "MyFirebase",
path: "Sources",
cSettings:
[.headerSearchPath("Headers")]),
.binaryTarget(name: "FirebaseCore",
path: "Sources/FirebaseCore.xcframework"),
.binaryTarget(name: "FBLPromises",
path: "Sources/FBLPromises.xcframework"),
.binaryTarget(name: "FirebaseAnalytics",
path: "Sources/FirebaseAnalytics.xcframework"),
.binaryTarget(name: "FirebaseAnalyticsSwift",
path: "Sources/FirebaseAnalyticsSwift.xcframework"),
.binaryTarget(name: "FirebaseCoreInternal",
path: "Sources/FirebaseCoreInternal.xcframework"),
.binaryTarget(name: "FirebaseInstallations",
path: "Sources/FirebaseInstallations.xcframework"),
.binaryTarget(name: "GoogleAppMeasurement",
path: "Sources/GoogleAppMeasurement.xcframework"),
.binaryTarget(name: "GoogleAppMeasurementIdentitySupport",
path: "Sources/GoogleAppMeasurementIdentitySupport.xcframework"),
.binaryTarget(name: "GoogleUtilities",
path: "Sources/GoogleUtilities.xcframework"),
.binaryTarget(name: "nanopb",
path: "Sources/nanopb.xcframework"),
]
)

5. Remove the Firebase online SPM reference by pressing the “-” button in the packages location

That’s it. Now the project is all set up and it should recognize the code you previously added to initialize the firebase SDK.

There are two more things to cover, taking care of known issues and uploading our new package to online instead of local.

Known Issues

  1. While changing the package.swift file some errors may appear and don’t disappear, In the menu bar click Product → Clear All Issues, it will cause the Xcode to refresh.

2. Derived Data not always being removed when building the Package of Project and it conflicts with the new changes, We need to delete it or reset the cache. File → Packages → Reset Packages Cache.

Uploading the package and using it from web or a private local git

  1. I have uploaded my repo to a new Bitbucket public repo on my account, and also created a Tag for the first version 1.0.0, Basically we need to add to our repository all of the files inside the Package folder (except if you have old git files so delete them first).

It should look like this:

2. Back to our project, we have two last changes to make — first: remove the local package we worked with until now. second: add the remote repo using the clone url from your newly uploaded package on the server.

The Final Result:

You can notice we have all of the xcframeworks of Firebase we need inside the package, so now we can use the same package across multiple projects without having to be worried there are copies of the Firebase SDK wondering around in our projects.

Conclusion

iOS Developers got a lot of challenges when working in a secured environment, There are pretty nice solutions for packaging as we experienced, I really hope you find my article interesting and learned something new. Having said that, I would really appreciate the feedback and also suggestions for better solutions for the issue we covered.

Article Materials — Link

--

--

Tomer Glick
Beta Blog

COE Lead of Digital Platforms / Guild Leader for Client Development