Swift packages are a great way to organize and share code, and are now supported while building apps for all Apple platforms in Xcode 11. Find out how to use community-developed packages in your project, how Swift packages are structured, and how package versioning and dependencies work.
In this session, I and my colleague, Balraj, will be talking about how to use Swift Packages from Xcode Projects.
The Swift Package Manager is part of the Open Source Swift tool chain. It was introduced in Swift 3.
And since then, a lot of Swift Packages have been created.
Also, a lot of Open Source libraries that were written for other package managers have been adapted to be compatible with the Swift Package Manager.
Swift Packages let you manage your versions of your dependencies, making sure that you get bug fixes without subjecting your code to source-breaking changes as the packages you depend on update.
Swift Packages are also a great way for your own -- to share code among your own apps, whether that's within a small team, a large organization, or maybe just among apps that you're working on by yourself.
And now in Xcode 11, you can access Swift Packages directly from Xcode projects.
We're excited too.
So in this session, we're going to start by talking about how to use a package, how to extend the functionality of an app by using a package.
We're then going to talk a bit about what exactly is in a package, how they construct it, what's in the data store -- in the file format of a package.
When we talk about package resolution, how Xcode fetches the right versions of packages and incorporates them into your app. We're going to talk then a little bit about updating packages, what happens when a new version of a package's update is published, and how you can take advantage of that. And we're going to talk about resolving any version conflicts that might happen as you update the packages.
So let's get started by extending NAP functionality to take advantage of packages.
Here, we have a little iPhone app that simply shows the, some of the lunch menu offerings of some of the cafes around where I work. We see that there are two here, entries. It's a SwiftUI app. So I can see the preview and Xcode without having to run my app.
So we see that the lunch menu from two different places are showing up. In the real version of this app, we would fetch this data over the network.
But in this demo app here, I just have them as local files among my source. So you could say that this menu is locally sourced.
So these two JSON files are showing up just fine. But this cafe here is a bit more modern and upscale. And so they have a YAML menu, right? So we're not able to parse that. And we're not seeing that.
So let's take a look at the source code that loads this data.
We see here that we handle JSON, but we don't handle YAML. So now fortunately, I know of a library that can parse YAML great. And it has a nice Swift interface. So it's called Yams. And I'm going to go and use that.
And to do that I bring down to file menu. I go to this new Swift Packages submenu, and Add Package Dependency. Now this submenu has a couple of other menu commands to deal with packages once they're in your app.
But you said Package Dependency.
And here, I see, because I added my GitHub accounts to Swift -- to Xcode's preferences, I see here all of the package repositories in that account.
And I also see any other repositories that I've starred.
In this case, I've starred Yams here.
But I could also, if I had a URL for a package, I could enter it here.
In this case, I'm going to click on the readme link here and go to the homepage for the Yams projects. That that looks good.
I'm going to go take a look here at the API. That looks like what I need.
Now, of course, when you're using an Open Source library, you're bringing somebody else's code into your app. So there are a lot of things you want to be careful with. You want to make sure you trust the source of that package.
You want to make sure that you know exactly what this library is doing, so you don't have any surprises.
And you want to make sure that the license for this Open Source library is compatible with your app's license.
So I've done all that. And so I'm going to go back to Xcode. And I'm going to just click Next here. And we're going to add a reference, a dependency on yams.
Now Xcode shows me the versions that are available. And it automatically sets me up to use the latest version of the package. We're going to talk a little bit more about the details of these options later on in the talk.
But most often, the default option here is the one you want. It's using version 2.0 anyway, up to, but not including, the next major version. I'm going to click Next.
Now Xcode is fetching the contents of the Yams package.
And it preselects for me the single product here. Some packages can have more than one product. In this case, there's one. It's a library.
Same name as the package.
And if you have more than one app in your project, you can choose where you want to link it. And in this case, it's just the one, so I'm going to link it to the Lunch app.
And I hit finish.
Now we see a couple of things here. For one, Xcode has added a reference to the Yams Package Dependency to the new Swift Packages tab in my Project Editor.
We also see that the Yams Package has shown up down here in Swift Package Dependency section.
We're not going to look in that package right -- just now. We're going to take a look in a moment. What we're going to do now, though, is to go into the food menu again. And now, we're going to make use of this from within our code. So I'm going to type import Yams here. And we see we have code completion against the import name. And I can also Command-Click on this import statement here. I can jump to the definition. And here I see the rendered version of the documentation for the interface for the Yams Project.
And all of this comes from the documentation comments in the source that was included in the package. So I go back to my code. I'm going to add another case here. Case, YAML.
I'm going to type -- just type YAML decoder. I have code completion for all of the library methods. And I have Quick Help, because the package includes that. So this looks and feels just like the built-in APIs in terms of the quality of the support you get.
I'm going to use this method.
I'm going to be using the same first parameters for the JSON case.
Now this API happens to take a string instead of data. So I'm going to be using that.
And I don't need the third parameter here, because I'll just use the default value.
So now, we can go back again to the list view.
And because this is a pretty big change, importing a new module, I'm going to hit resume. Xcode is going to rebuild the application in the background. And I'm going to see the preview here.
And now we see that I see the contents from the YAML data file as well.
All right, so now I can commit this to my repository.
And we see here what we expect. We see the source changes. Let me make that a bit bigger.
We see the source changes that I made, of course and it's part of the commit sheets.
We also see that the project file has changed, because I've added the reference to the Yams Package.
And we see one more thing here, which is Xcode has created a directory called SwiftPM, that's for the Swift Package Manager, underneath the shared data, underneath the workspace. And you want to check that in, because we'll talk about what exactly is in there later on in the talk. But Xcode stores information about the package versions that resolved. And you want to check that in so that everybody on your team gets the same versions.
All right, so now I can check this in and say useYAML.
And use. All right, go back to slides.
All right, so we saw quickly how to use an Open Source Package from within a project. Let's take a closer look at what's in the YAML Package.
The Package is a directory that contains a Swift Package manifest.
The manifest is a file called Package.swift, and it identifies that directory as a Swift Package.
It also contains sources and of course, it contains unit tests to make sure that those sources continue functioning well.
Underneath the sources is a subdirectory for each of the separate targets in the package. These are the separately buildable components of the package.
And similarly, under the test directory, there's a separate subdirectory for every test suite.
So let's take a closer look at what's in one of the target directories.
Each of the targets can have implementation that's either one of the C-based languages or Swift. So in the case of YAML, there's a core CYAML parser that's written in C.
It could also contain Objective-C++ files.
And then there's a Swift interface in a separate target on -- in addition to that, that calls down into the CYAML code.
And then the unit tests here are written in Swift.
So if we look at the contents of the Swift Package Manifests, the first line here is a declaration of what the tool version the package needs. So this says the minimum version that can parse this package manifest at all.
The package description API is a declarative API that is provided by the Package Manager's package description library.
And by importing that, then the rest of the contents of this file can declare the characteristics of the package.
That includes the name of the package.
It also includes a section that lists the products that the package vends to the clients.
So the Package can control which parts of its code can be directly imported by the client. And in this case, there is one library with the same name as the package, so called Yams.
We'll talk in a moment about the targets part there. It basically says that this library publishes the Yams target to clients as a library.
The target section lists the individually buildable parts of the package. And as we see here, there is a one-to-one correspondence between the source folders and the targets.
Each of those source folders can have other subfolders for organizational purposes, but the top level under sources is one folder per packet, per target.
In this case, we see the CYAML target listed without any dependencies.
And the Yams target is listed as depending on the CYAML target. This means that when the product refers to Yams, that will, in turn, indirectly bring in CYAML as well.
And then there's one test target here for the unit tests.
This code won't actually be linked into the client.
But it's necessary to make sure your library keeps running properly.
And in this case, the Yams Package Manifest also lists some older Swift versions that the code is compatible with.
There are also other characteristics that you can specify and in this declarative language, and we'll get to some of those later.
So how is this then linked into your app when you actually build and run your app? So your project consists of source files.
So it could be Swift. It could be other languages.
And the packages you depend on, they're also source files.
And so what Xcode does is it takes all of these source files, and it compiles them and particularly compiles the package code in a way that is compatible with the app code in your project.
So this includes architectures, platforms, those things.
It will recompile it multiple times, if needed, depending on what your apps need.
Then it links it in and combines all of that into the application.
Package libraries are static by default.
And so all the code is linked together.
And this is repeated for the various apps in your project that use the same package. So if you have an iOS app and WatchOS app, they use the same package. Xcode might build the code multiple times as needed for each of those apps.
Now, we saw a case where a project can depend on a package.
And we saw that is shown in the Package Dependencies part of the target editor.
But a package can also depend on other packages. And this is done through the package manifest.
So one of the sections of the package manifest the Yams didn't have was a Dependencies section. It didn't have that because it doesn't actually depend on other packages.
But some Packages that you have might. And so the Package Dependency graph can include both direct and indirect dependencies.
Now, I mentioned before that you can manage your versions with pack -- the Package Manager.
And this uses something called semantic versioning. And that's a fairly widely used strategy that assigns semantic meaning to each of the components of a three-part version.
So in this case, for example, the major version is incremented whenever there are breaking changes to the API.
So this is anything that would cause the clients to have to be modified. For example, if you rename a method or remove a method or if a package makes a met -- a semantically meaningful change that will cause existing clients to have to adapt.
This is the reason why the initial version of restriction goes up to, but not including, the next major version number.
The minor version number of a package is incremented when functionality is added in a way that doesn't break existing clients. So this could be adding a method, for example.
And finally, the patch version here is when there are bug fixes that don't have any semantic changes, semantic meaning changes.
And packages can be safely updated to bug fixes, to incorporate bug fixes without changing the semantics of the app.
Okay, so we've seen how to use the package. And we've taken a closer look at that package. And now I'd like to invite my colleague, Balraj, on the stage to talk about Package Resolution in more detail.
Thank you, Anders.
Package resolution is the process Xcode goes through when selecting the versions of Packages to use inside of your workspace.
Let's go into a little bit more detail in how that was working with -- and how that was working in the Lunch project Anders was demoing earlier.
So here in the Project Editor in the Swift Packages tab, we can see our dependency on -- our Lunch's dependency on Yams. Using the Version Rule 2 to up to the next major version, meaning any version of Yams from 2 up to, but not including 3.
Then, in the Swift Package Dependencies section of the Project Navigator, we can see Yams at version 2.0.
Let's look a little deeper into this.
Lunch is selecting Yams at Version 2, because of its version requirement, 2 up to the next major version.
If a version 2.1 existed, Xcode would have selected that instead, as 2.1 is the latest version matching our version requirement.
However, if a version 3 existed, Xcode would not have selected it, as it doesn't fit into the constraints that we specified.
In this case 2.1 and 3 are hypothetical examples. And you'll see Yams continue to resolve at Version 2 throughout the rest of this presentation.
In this example, there's one package with one version to choose from.
So let's go into a few more interesting cases, where package resolution can get a little bit more complicated.
So here is the Lunch app that we have today.
It has a very basic UI and is only using one package, as we just said.
My team, over time, adds more packages to our application and they use these packages to show common design themes across all the applications that my team owns. So when we go back to the Lunch application, after a few weeks, we can see that the UI has been updated, and there are three additional packages inside of our workspace.
These three packages are DesignFont, DesignTheme, and DesignColor.
All resolving at their own respective versions.
So Xcode did a lot of the work for us in terms of selecting the versions of these packages. But I want to understand why these packages are resolving at these versions.
So in order to do so, I go back to the Project Editor.
Here in the Swift Package Dependency section, we can see our new dependency on DesignTheme, with a Version Rule 1 one up to the next major version.
Xcode, in this case, selects DesignTheme at Version 1, because it's resolving from 1 to the next major version.
And so we also see our dependency on Yams is still the same.
And you might be wondering.
Where are DesignFont and DesignColor in this case? We're looking in the Project Editor, but we don't see them inside of there.
Well, the reason for this is the Project Editor shows us all of the direct dependencies between the Lunch application and its direct packages.
So in order to look at our package's dependencies, we want to go and look at the DesignTheme package.
This is because it's the newest package that was added to our workspace. And it's safe to assume that they're not coming from Yams.
So in order to do so, we go back to the high-level view of Xcode, and look under the Swift Package Dependencies section. Here we see our package DesignTheme.
We disclose the package and see all the content available inside of there. And in this case, we want to look at the Package.swift manifest file.
This is because it is where we will find all of our dependency information for this package.
So we go to this file. And in the dependencies array, we see DesignFont and DesignColor, and their version requirements.
DesignFont is being resolved how we've seen before, 1 up to the next major version.
And so Xcode will select version 1.2 as it's the latest version of the package.
DesignColor is resolved a little bit differently.
This is using 1 up to the next minor version, meaning any version of designs color from 1, and up to, but not including, 1.1.
This is typically used when packages want to be a little bit more conservative with the new versions that they take during updates.
And so Xcode selects Version 1.0.1 of the DesignColor package.
So now this is the full view of the new packages that have been added and why they're being resolved at the versions they are. You'll remember the last step Anders did in the previous demo was importing Yams and then using its APIs.
Let's talk about how this works and how it relates to package resolution.
All of our packages are producing libraries of the same name.
And we can see that Lunch is importing content from DesignTheme and it's also importing content from Yams.
And then the DesignTheme library is also importing content from DesignFont and DesignColor.
When we look at the full graph here, we can see this is very similar to how package resolution is working.
And that is intentional.
When we added our direct dependencies on our packages, we also ended up importing their content and actually using the APIs in our application.
But what happens when I want to import content from a sub dependency into our project? So let's talk about how we would do this.
We have our Lunch project, which is depending on DesignTheme.
And DesignTheme is depending on the DesignFont package.
The DesignFont package is producing a library of the same name.
In this context, we don't want to immediately import content from DesignFont into Lunch, because if DesignTheme loses its dependency on DesignFont and Update, Xcode will lose its reference to DesignFont. And now, we won't be able to use the DesignFont library.
So a better approach of doing this, and we'll go back to the beginning of our example, is to create a direct depend -- direct package dependency between the Lunch project and the DesignFont package.
And then we can import content from DesignFont into Lunch, because if DesignTheme loses its dependency on DesignFont in an update, we still retain the reference to that library inside of Xcode.
So that is how Xcode selects different versions of your packages.
Let's go into an -- into how you can go about getting new versions of those packages, which provide API improvements and bug fixes.
So one day, I'm at team with -- I'm at lunch with the team maintaining DesignFont. And they tell me about a new version of the DesignFont package, which has a few small bug fixes, Version 1.2.1. When I get back to my desk, I see DesignFont is using version 1.2. The Lunch project is using DesignFont at Version 1.2.
And so I want to update this package.
In order to do so, I click File, Swift Packages. And here, I'm brought with multiple options regarding Swift Packages.
In this case, I want to update to the latest package versions.
So go ahead and click that, and the update operation occurs.
We're now using DesignFont Version 1.2.1.
So what exactly does updating package version do? What happened during that update operation? So there exists a file called Package.resolved, which is central to this.
Package.resolved records version information about all the packages inside of your workspace.
And when you go through the update operation, this file gets updated, and then Xcode will select -- pull down the new versions for you.
This file exists inside of xcsharedata, which is typically shared with your team and source control.
So an important thing to note here is that that update operation we just did is a local operation.
In order to share this update across my team, I have to commit and push my changes to the Package.resolved file.
And then if you want to look for all of this yourself, it's inside of the Xcode Project file.
But please note that you don't actually have to edit the Package.resolved file yourself.
Xcode should be doing all of the work for you.
So as I said before, we actually haven't shared this update with our team. So let's do that.
We can do all of this within Xcode by going to the Source Control Menu and clicking Commit.
Inside of here, we'll see all the changes that were made to the Package.resolved file and how it updated from Version 1.2, what we were previously using, to 1.2.1, the new version.
Because I want to push, I go ahead and click the lower-left checkbox, push to remote.
And then I can commit and push my changes.
We've now successfully shared this update across our team.
So all of this begs the question, why do we need a Package.resolved file? This file exists to make sure that when people are using -- people across my team are using the Lunch project at the same commit, that they're all getting the same version of DesignFont.
If this file -- if the Package.resolved file didn't exist, you might end up in a situation where multiple people on your team are using the same version of your project, but without consistency across the versions of packages that you're using.
So an incredibly important thing to note is to check in your Package.resolved file.
Otherwise, you might end up using inconsistent versions of packages across your team. So it's incredibly important that this file is checked in.
And then be sure to look out for new versions of your packages.
And then, update with intent. Otherwise, you might miss out on a few key bug fixes and API improvements that your packages are providing for you.
So that's how you update packages.
Let's go into a little bit more of an advanced example on how to resolve package conflict, which will bring together a lot of the different topics we've talked about today.
So I launch a project to change the font that we're using for our price inside of the Lunch application.
The system font that I want to use is available in DesignFont at Version 2, and this helps keep a consistent version of the fonts we're using consistent across the applications our team owns.
So let's do this.
In order to use the DesignFont package, we need to create a direct dependency between Lunch and DesignFont.
And then, because DesignFont at Version 2 is what released this new font, we want to use that version specifically.
So we go to Xcode, and we go through the Add Package workflow.
We click DesignFont as it's in our favorite accounts. And we say Next.
We choose to up to the next major version, because we want to be open to any updates or new versions that DesignFont releases. And then we click Next.
Here, we run into a package resolution error, because of our new dependency requirement on DesignFont from 2 to the next major version.
So let's look into what's happening here and debug what's going on.
One thing that's really important to remember when debugging package resolution is to look at the full picture.
We want to think of every requirement that is that is affecting our packages and not just narrow in on what we've just added.
So we take a step back to how this was working before.
Lunch had a great dependency on DesignTheme at Version 1.
And DesignTheme had a direct dependency on DesignFont at Version 1.2.1, using the version requirement 1 to the next major version.
And then when we added our direct dependency between Lunch and DesignFont from 2 up to the next major version, we ran into the package resolution conflict.
You'll notice that DesignTheme's requirement on DesignFont 1, up to, but not including, 2. And Lunch's requirement on DesignFont 2, but not in -- up to, but not including 3, can't select one version that matches both of those requirements.
And so in SwiftPMs integration into Xcode, you can only have one version of a package in a workspace.
And this is explaining why we're seeing the package resolution error here.
Xcode can't possibly pick one version that satisfies both version requirements.
Going about fixing this is really situation specific.
But typically, when I run into package resolution errors like this, I want to look at the newer versions of packages that are available to me.
And then I can see if those newer versions have provided any updates to their sub dependencies.
In this case, I noticed that there's a Version 2 of DesignTheme that we haven't really looked at yet.
So let's go to GitHub and look at our DesignTheme's version requirements.
When we go to GitHub, we can look in the dependencies array of the Swift Package Manifest.
And here, we see that DesignFont's version requirement has updated from 2 -- from 1 to the next major version to be 2 to the next major version.
This matches with the version requirement that we were trying to add between Lunch and DesignFont.
So if we can update DesignTheme's version requirement to be resolving at Version 2, we can now satisfiably add a direct dependency between Lunch and DesignFont.
So let's go about doing that and update the major version of DesignTheme.
Previously, Lunch was using DesignTheme up from 1 to the next major version. We want to change that so that it's now using 2 up to the next major version.
So we go back to Xcode. And now, we just click on the DesignTheme package inside of the Project Editor. And then we're back to an edit version rule sheet.
Here, we were previously specifying 1 up to the next major version.
But now we want to change that to be 2 to the next major version.
So we simply change our 1 to a 2 and then click Done.
And now, the update operation occurs. And we can see that DesignTheme is at Version 2, and DesignFont is at Version 2.
But one important thing happens here. When we update from one major version to another, we run into a build failure.
And this is because when going from one major version of a package to another, there can be API changes. And this can potentially cause breaking changes inside of your project.
This means that when you update from one major version to another, you should be prepared to change some APIs, or change however the APIs are working inside of the new version of the package.
This can be very small changes, or it can be a little bit more involved.
In this case, we've done all the work for you to spend more time on packages. So when we go back to the Lunch application, our build errors are resolved. And we're using the DesignTheme at Version 2 successfully.
So our build is now succeeding.
And we want to add a direct dependency between Lunch and DesignFont.
Because now that we're at Version 2 of DesignTheme, we can add the version requirement we wanted to.
So we go back to Xcode, and we go through Add Package workflow.
We pick DesignFont. We select 2 to the next major version.
And now, we can link the DesignFont library with our application.
And now inside of the Project Editor, we can see that we're successfully using DesignFont from 2 to the next major version.
Now that we've done this, we can now import the content inside of DesignFont, successfully use its library, and then with a few small code changes, to update the font of the price.
So we've now successfully done that and resolved our package conflict.
We've updated our package versions, and we've successfully went over how to debug package resolution inside of Xcode.
So we went over a lot of things today.
We talked about how you can start using Open Source packages inside of your projects and quickly start using its APIs.
We took a closer look at what a package is, what -- and how the Package.swift manifest brings it all together.
We went over how Xcode selects the versions of packages to use inside of your project.
And then we went over how you can keep those versions up-to-date and keep getting new updates for your packages.
We then took a look at a more advanced example of how to resolve package conflicts, which taught us how to debug packages, and how to update package versions.
There are a ton of packages already available on GitHub today.
And we recommend that you will look through these packages and see where you can incorporate them in your existing applications.
But we're not done there. There's a session tomorrow that my colleagues, Ankit and Boris, are presenting on, on how to create Swift Packages.
This session will go into even more detail about what a package is, how to edit packages, the SwiftPM Open Source tool, and much, much more.
This session will help you become even more of an expert on how packages work inside of Xcode, and you won't want to miss it. And then if you have any more questions for the SwiftPM team, we'll be at the Swift Open Hours Lab right after this session.
And then also, there are two more sessions throughout the week, one on Thursday at 12:00 in the Swift Packages Lab, and another of the same name at Friday at 12:00.
Thank you so much for coming today, and we hope you have a great rest of your week. Thank you.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.