7.
Multi-Module App
Written by Aaqib Hussain
In the last section, you added new features to the app using SwiftUI and Combine based concepts such as ObservableObject and @FetchRequest. You used these concepts to implement features like Search and Locating animals near you. These features made the app more usable. But what about making our code more reusable?
In this section, you’ll learn how to modularize the app and navigate between different modules. In this chapter, you’ll learn about the benefits of modularization and what tools are at your disposal to support it.
More specifically, you’ll learn about:
- Xcode support in the build process using build targets and workspace compilation.
- The different types of frameworks you can create for your apps.
- Some of the dependency management options available for iOS development
With the acquired skills, you’ll create an onboarding framework for PetSave. So users can have a nice introduction to your app.
Onboarding screens welcome users the first time they launch your app. Since their first impression could be the last impression, it’s quite important for the developers to get this right.
Are you excited to get onboarded? Here you go!
Modularization
Modularization is a software design technique that lets you separate an app’s features into many smaller, independent modules. To achieve modularization, one must think out of the box. You use encapsulation and abstraction by exposing the methods you want the app to use and hiding all the unnecessary or complex details.
Benefits of modularization
Modularization comes with many benefits, including:
- Reusability
- Time and cost savings
- Community support
- Improved build time
Using modularization helps you write more robust and scalable code. Now, you’ll go over each one of the benefits to understand what they can mean for your codebase.
Reusability
Consider you develop an onboarding framework where you customize texts and images before showing them in the app. To create such a framework, you must implement it so it’s independent of the app. Then, you share and reuse it in other projects.
Time and cost savings
Reusability leads to time and cost savings. In the example of creating an onboarding module, you can easily integrate the onboarding module into a different project. It’s like plug and play that saves you both time and development cost.
Community support
By publishing the onboarding module as public on a platform like GitHub, you get support from the open-source community on fixing bugs you might have missed. Developers simply open a pull request for a bug fix or add a new feature.
Build time
When you rebuild the project after changing the onboarding framework, Xcode won’t recompile the entire app. Instead, it’ll only compile the changed module. This results in faster build times and, in general, accelerated development. But for you to guarantee this, you have two know the different kinds of frameworks and the use case for each one, you’ll go over that later in this chapter.
Xcode support in the build process
While Xcode comes with many features, the two features you’ll learn about in this chapter are build targets and workspace compilation.
Build targets
A target is a modularized structure. A target takes its instruction through build settings and build phases. A project can contain more than one target, and one target can depend on another. These targets can be something like the watchOS version of your app or represent your app test suite.
If one target is dependent on another, and they’re in the same workspace, Xcode automatically builds the one the other depends on first. For example:
Consider target A and target B. If B is dependent on A, then Xcode will build A first. Such a relationship is an implicit dependency.
Suppose two of your targets depend on a single dependency, and you want to link one target to a different version. In that case, you add an explicit dependency in the build settings to override the implicit dependency.
A typical Xcode project contains Main, Unit test and UI test targets. When you add a framework to your project, it’s added as a separate target.
Workspace compilation
A workspace combines projects and other documents under one roof so you can work on them together. It can have multiple projects or documents you want to work on. It also manages implicit and explicit dependencies among the included targets.
A workspace holds references to all the files included in each of its contained projects. Then, it handles tasks like indexing files, code completion and jump to definition.
You can refactor a piece of code in a framework and see the changes throughout the targets using it in one go. All the projects inside a workspace share the same build directory. Thus, all files are visible to each other. If two of your targets use the same dependency, you don’t need to copy it in both projects. Xcode is intelligent enough to copy it just once.
Note: Apple uses the term framework to refer to modules. You’ll see the term framework instead of module throughout this chapter and later on.
What is a framework?
A framework is a bundle that can contain resources of any type, such as classes, assets, nib files or localizable strings. Frameworks encapsulate and modularize code, making it reusable. Common iOS frameworks include Foundation, UIKit and SwiftUI.
Types of Frameworks
There are two types of frameworks in iOS: Static and Dynamic. Take a moment to learn about these frameworks and how they differ.
Static Framework
Static frameworks consist of code that doesn’t change because it’s linked at compile time. Static frameworks generate a .a extension. They only hold code and gets copied with the app’s executable, making the executable size larger.
It has faster function calls, even when you need to call just one function the entire framework is present in the app. Thus, this type of framework guarantees its presence in the app. When you change the framework, the entire app recompiles.
The networking layer you built earlier in the book, could be considered a classic example of creating a static framework.
Dynamic Framework
Unlike static frameworks, dynamic frameworks have a codebase that may change and contain other resources, like images. Dynamic frameworks generate the extension .dylib. It’s not copied, but linked with the app’s executable at runtime, thus, resulting in a smaller app size.
As the name suggests, these frameworks are dynamic, so the code only loads when it’s needed. The system usually holds a single copy of the framework. The apps then share this common framework’s copy.
Function calls are slower because the framework is located outside the app and shared. When you change it, you don’t recompile the entire app. Instead, you only compile the framework.
To check the type of framework, open the Final project. In Project target, select PetSaveOnboarding. Go to Build Settings ▸ Linking ▸ Mach-O Type. Here, you’ll see the framework is dynamic, but you could change it in the dropdown if you wanted to.
Resources are a classic example of creating a dynamic framework. Next, you’ll create a dynamic onboarding framework.
Note: Previously, Apple only supported static frameworks. With iOS 8+, Apple allowed the use of dynamic frameworks. You can read more about frameworks in the official documentation.
Creating a dynamic onboarding framework
It’s finally time to start coding your very first framework! Your goal is to reach this:
Open the starter project. Click File ▸ New ▸ Target in iOS. Then, in the filter, type Framework.
Select Framework and click Next. Give the product name PetSaveOnboarding. Make sure to uncheck Include Tests and Include Documentation as you don’t need them for this chapter. Then click Finish.
Note: At the time of writing, the latest Xcode version was 13.2, which selects iOS 15.2 as the framework’s deployment target by default. To avoid any future problems, change this target to iOS 15.0.
You’ll see a PetSaveOnboarding in the project navigator:
Next, create a group named Resources under PetSaveOnboarding. Then, in this chapter’s materials, find the assets folder.
You’ll see some pet and color resources. Drag and drop the contents of assets into Resources.
In Add to targets, choose PetSave and PetSaveOnboarding. Then click Finish.
The project directory now looks like this:
Still in PetSaveOnboarding, create a group named Extensions. Then create three files: Bundle+Extension.swift, Color+Extension.swift and Image+Extension.swift.
Open Bundle+Extension.swift and add:
extension Bundle {
public static var module: Bundle? {
Bundle(identifier: "com.raywenderlich.PetSaveOnboarding")
}
}
This bundle refers to the framework using its identifier. It helps in accessing the assets.
Then, open Color+Extension.swift and add:
import SwiftUI
extension Color {
static var rwGreen: Color {
Color("rw-green", bundle: .module)
}
static var rwDark: Color {
Color("rw-dark", bundle: .module)
}
}
Here, you add the default theme colors that you’ll use for styling the buttons and the page controls.
Finally, open Image+Extension.swift and add:
import SwiftUI
public extension Image {
static var bird: Image {
Image("creature-bird-blue-fly", bundle: .module)
}
static var catPurple: Image {
Image("creature-cat-purple-cute", bundle: .module)
}
static var catPurr: Image {
Image("creature-cat-purr", bundle: .module)
}
static var chameleon: Image {
Image("creature-chameleon", bundle: .module)
}
static var dogBoneStand: Image {
Image("creature-dog-and-bone", bundle: .module)
}
static var dogBone: Image {
Image("creature-dog-bone", bundle: .module)
}
static var dogTennisBall: Image {
Image("creature-dog-tennis-ball", bundle: .module)
}
}
This extension refers to all the image assets from the assets you added.
Now, create a group named Model. Under it, create files named OnboardingModel.swift and Pet.swift.
Open OnboardingModel.swift and add:
import SwiftUI
public struct OnboardingModel: Identifiable {
public let id = UUID()
// 1
let title: String
let description: String
let image: Image
// 2
let nextButtonTitle: String
let skipButtonTitle: String
// 3
public init(
title: String,
description: String,
image: Image,
nextButtonTitle: String = "Next",
skipButtonTitle: String = "Skip") {
self.title = title
self.description = description
self.image = image
self.nextButtonTitle = nextButtonTitle
self.skipButtonTitle = skipButtonTitle
}
}
Here’s a code breakdown:
- Creates
title,descriptionandimageto hold the data that appears on each page of the onboarding screen. - These properties hold the titles for the Next and Skip buttons shown on each page.
- This is an initializer with default titles for the buttons.
Now, open Pet.swift and add:
import SwiftUI
// 1
struct Pet: Identifiable {
let id = UUID()
let petImage: Image
let position: CGPoint
}
// 2
extension Pet {
static let backgroundPets: [Pet] = {
let bounds = UIScreen.main.bounds
return [
Pet(petImage: .bird,
position: .init(x: bounds.minX + 50, y: 20)),
Pet(petImage: .catPurple,
position: .init(x: bounds.maxX, y: bounds.maxY / 2)),
Pet(petImage: .catPurr,
position: .init(x: bounds.maxX, y: bounds.maxY - 100)),
Pet(petImage: .chameleon,
position: .init(x: bounds.minX, y: bounds.maxY / 2)),
Pet(petImage: .dogBoneStand,
position: .init(x: bounds.minX, y: bounds.maxY / 1.5)),
Pet(petImage: .dogBone,
position: .init(x: bounds.maxX - 50, y: 50)),
Pet(petImage: .dogTennisBall,
position: .init(x: bounds.minX, y: bounds.maxY - 10))
]
}()
}
Here’s what you added:
- This structure contains the pet’s image and the position shown on the onboarding background view.
- A list of pets with some tried and tested positions that look good on the view.
Now that the initial setup is complete, you’ll add your first view to the framework.
Under PetSaveOnboarding, create a SwiftUI view named OnboardingView.swift and replace the default code with:
import SwiftUI
struct OnboardingView: View {
// 1
let onboarding: OnboardingModel
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 12, style: .circular)
.fill(.white)
.shadow(radius: 12)
.padding(.horizontal, 20)
VStack(alignment: .center) {
VStack {
// 2
Text(onboarding.title)
.foregroundColor(.rwDark)
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
.padding(.horizontal, 10)
Text(onboarding.description)
.foregroundColor(.rwDark)
.multilineTextAlignment(.center)
.padding([.top, .bottom], 10)
.padding(.horizontal, 10)
onboarding.image
.resizable()
.frame(width: 140, height: 140, alignment: .center)
.foregroundColor(.rwDark)
.aspectRatio(contentMode: .fit)
}
.padding()
}
}
}
}
This is the base view of the onboarding screen. It:
- Holds the onboarding model’s object.
- Sets the title, description and image from the model to the UI.
Then, add another SwiftUI view and call it OnboardingBackgroundView.swift. This view acts as a background for the OnboardingView. Replace the code with:
import SwiftUI
struct OnboardingBackgroundView: View {
// 1
let backgroundPets = Pet.backgroundPets
// 2
var body: some View {
ZStack {
ForEach(backgroundPets) { pet in
pet.petImage
.resizable()
.frame(width: 200, height: 200, alignment: .center)
.position(pet.position)
}
}
}
}
Here’s a code breakdown:
- An array holding all pets displayed in the background.
- Displays each pet on a view.
Now, create another SwiftUI view named PetSaveOnboardingView.swift and add the following code before body:
@State var currentPageIndex = 0
// 2
public init(items: [OnboardingModel]) {
self.items = items
}
// 3
private var onNext: (_ currentIndex: Int) -> Void = { _ in }
private var onSkip: () -> Void = {}
// 4
private var items: [OnboardingModel] = []
// 5
private var nextButtonTitle: String {
items[currentPageIndex].nextButtonTitle
}
private var skipButtonTitle: String {
items[currentPageIndex].skipButtonTitle
}
Here’s what you added:
- Holds the current index of the onboarding view.
- The initializer to read the onboarding model array.
- A completion handler for listening to Next and Skip button actions.
- Holds the array that contains all of the onboarding models.
- Titles for the Next and Skip buttons.
Now, replace body with:
public var body: some View {
if items.isEmpty {
Text("No items to show.")
} else {
VStack {
TabView(selection: $currentPageIndex) {
// 1
ForEach(0..<items.count) { index in
OnboardingView(onboarding: items[index])
.tag(index)
}
}
.padding(.bottom, 10)
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.onAppear(perform: setupPageControlAppearance)
// 2
Button(action: next) {
Text(nextButtonTitle)
.frame(maxWidth: .infinity, maxHeight: 44)
}
.animation(nil, value: currentPageIndex)
.buttonStyle(OnboardingButtonStyle(color: .rwDark))
Button(action: onSkip) {
Text(skipButtonTitle)
.frame(maxWidth: .infinity, maxHeight: 44)
}
.animation(nil, value: currentPageIndex)
.buttonStyle(OnboardingButtonStyle(color: .rwGreen))
.padding(.bottom, 20)
}
.background(OnboardingBackgroundView())
}
}
Here is what this does:
- Creates
OnboardingViewto show each onboarding item in the array. - Next and Skip buttons added to the view.
Right after body add this:
// 1
public func onNext(
action: @escaping (_ currentIndex: Int) -> Void
) -> Self {
var petSaveOnboardingView = self
petSaveOnboardingView.onNext = action
return petSaveOnboardingView
}
public func onSkip(action: @escaping () -> Void) -> Self {
var petSaveOnboardingView = self
petSaveOnboardingView.onSkip = action
return petSaveOnboardingView
}
// 2
private func setupPageControlAppearance() {
UIPageControl.appearance().currentPageIndicatorTintColor =
UIColor(.rwGreen)
}
// 3
private func next() {
withAnimation {
if currentPageIndex + 1 < items.count {
currentPageIndex += 1
} else {
currentPageIndex = 0
}
}
onNext(currentPageIndex)
}
Here, you add:
- Create
onNext(action:)andonSkip(action:)to listen to the buttons’ actions. - This method sets the selected page control indicator color.
- Changes the index of the current onboarding view with animation.
At the end of the file, add this:
struct OnboardingButtonStyle: ButtonStyle {
let color: Color
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(color)
.clipShape(Capsule())
.buttonStyle(.plain)
.padding(.horizontal, 20)
.foregroundColor(.white)
}
}
This is the custom style you used for the buttons on PetSaveOnboardingView.
Note: When creating frameworks, the structures or classes can use a public access modifier so that other projects can call them.
To preview the views you created so far, create a private extension like this:
private extension PreviewProvider {
static var mockOboardingModel: [OnboardingModel] {
[
OnboardingModel(
title: "Welcome to\n PetSave",
description:
"Looking for a Pet?\n Then you're at the right place",
image: .bird
),
OnboardingModel(
title: "Search...",
description:
"Search from a list of our huge database of animals.",
image: .dogBoneStand,
nextButtonTitle: "Allow"
),
OnboardingModel(
title: "Nearby",
description:
"Find pets to adopt from nearby your place...",
image: .chameleon
)
]
}
}
Here, you initialize the mock onboarding model with the values shown on each page.
Then, replace PetSaveOnboardingView_Previews implementation with this:
struct PetSaveOnboardingView_Previews: PreviewProvider {
static var previews: some View {
PetSaveOnboardingView(items: mockOboardingModel)
}
}
This code previews the view using mock data.
The preview on your canvas shows this:
That’s so cool!
Now it’s time to use the framework you created in the app.
The onboarding screens for any app usually show up once, the first time the user launches the app. You’ll now build this logic by
saving the app’s state in UserDefaults.
Open AppUserDefaultsKeys.swift and add the following property to the already existing enum:
static let onboarding = "onboarding"
This key saves the state of each user’s action to indicate if they’re launching the app for the first time or a subsequent time.
Next, open AppMain.swift and import the PetSaveOnboarding framework:
import PetSaveOnboarding
In the existing structure, add:
// 1
@AppStorage(AppUserDefaultsKeys.onboarding)
var shouldPresentOnboarding = true
// 2
var onboardingModels: [OnboardingModel] {
[
OnboardingModel(
title: "Welcome to\n PetSave",
description:
"Looking for a Pet?\n Then you're at the right place",
image: .bird
),
OnboardingModel(
title: "Search...",
description:
"Search from a list of our huge database of animals.",
image: .dogBoneStand
),
OnboardingModel(
title: "Nearby",
description:
"Find pets to adopt from nearby your place...",
image: .chameleon
)
]
}
Here’s what you added:
-
@AppStorageis a SwiftUI property wrapper that works hand-in-hand withUserDefaults. It saves the value ofshouldPresentOnboardinginUserDefaults. - The model data to show the first time of app launch.
Next, update the body scene like this:
var body: some Scene {
WindowGroup {
ContentView()
// 1
.fullScreenCover(
isPresented: $shouldPresentOnboarding, onDismiss: nil
) {
// 2
PetSaveOnboardingView(items: onboardingModels)
.onSkip { // 3
shouldPresentOnboarding = false
}
}
}
}
Here’s a code breakdown:
- Shows the full-screen cover if
shouldPresentOnboardingistrue. - Presents
PetSaveOnboardingViewwith the model data. - On Skip button tap, set
shouldPresentOnboardingtofalseto avoid showing the onboarding again.
Finally, build and run. You’ll see the onboarding screen looks like this:
Nicely done, you deserve a pat on the back!
Now that you had developed a framework, you need to decide how to distribute it. There are multiple options for developers to manage and distribute their frameworks and libraries. Swift Package Manager is the de-facto choice for handling dependencies since Apple introduced it with Xcode 11.
In the remaining segments of this chapter, you’ll go over the following tools and concepts:
- Cocoapods.
- Carthage.
- Swift packages.
- Differences between dependency managers.
- Creating and configuring a Swift package.
- Adding code and resources to the package.
- Publishing the package to GitHub.
- Replacing the PetSaveOnboarding framework with the published Swift package.
What is Cocoapods?
Cocoapods is a dependency manager that supports publishing and maintaining libraries in Swift and Objective-C. You can use it to import multiple libraries in your project. It’s built with Ruby, and you can use the default version of Ruby on Mac to install it.
Using Cocoapods
There’s a large variety of third-party libraries written with Cocoapods on GitHub. To consume these libraries, initialize Cocoapods in your project and put all your dependencies in a file called Podfile.
Once you install your dependencies using:
pod install
Cocoapods will create a .xcworkspace containing all your source code and dependencies.
Note: To read in detail about Cocoapods visit the Cocoapods website.
What is Carthage?
Like Cocoapods, Carthage is a dependency manager. It’s the first one to support Swift that was also written in Swift. It supports macOS and iOS applications.
You need to install Carthage and follow a similar process as the one you do for Cocoapods by indicating your dependencies in a file called Cartfile, then you run:
carthage update --use-xcframeworks
In your project, this command generates a file named Cartfile.resolved and a directory named Carthage. The Carthage/Build directory contains the built frameworks as an .xcframework.
Note: This tutorial will help you get started with Carthage. Carthage also has very detailed documentation on GitHub.
What are Swift packages?
Swift Packages are repositories that enable developers to create, publish and maintain a package. Furthermore, they help to add, remove and manage Swift package dependencies. Besides Swift language, they allow porting of code from Objective-C, Objective-C++, C or C++.
Swift packages use the open-source project Swift Package Manager or SPM. The Swift team introduced SPM in Swift 3.0. They came up with a tool to manage the distribution of code. SPM downloads, compiles and links libraries. It’s an integral part of the Swift build system and provides a good alternative to other package managers like CocoaPods.
When you create a Swift Package, it comes with a Sources folder and a manifest file called Package.swift.
Package.swift describes the package and contains configuration information such as the package name, libraries, executables, dependencies and targets.
Differences between dependency managers
You’ve so far studied Cocoapods, Carthage and Swift Package. Here, you’ll learn the basic differences between them:
| Properties | Cocoapods | Carthage | Swift Package |
|---|---|---|---|
| Agnostic of the project | ❌ | ✅ | ✅ |
| Easy to manage | ❌ | ❌ | ✅ |
| Supported by Apple | ❌ | ❌ | ✅ |
| Thousands of open source libraries | ✅ | ✅ | ❌ |
| Requires manual setup | ❌ | ✅ | ❌ |
| Supports dynamic and static frameworks | ✅ | ✅ | ✅ |
| Faster build time | ❌ | ✅ | ✅ |
| Dependent dependency management | ✅ | ✅ | ✅ |
Apple supports and recommends Swift package, and Xcode provides ease with integrating it.
Note: If you’re interested in learning about creating your own Cocoapods or Carthage, check out the documentation on Cocoapods and Carthage.
Now that you’ve gained enough theoretical knowledge about Swift packages, it’s time to get cracking with some practicals.
Creating and configuring a Swift package
Start by selecting Package from File ▸ New.
Name the package PetSaveOnboarding. Then, select create Git repository on my Mac and click Create.
This opens a new window. In the project navigator, you see:
Open Package.swift and replace the content with:
// 1
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of
// Swift required to build this package.
import PackageDescription
let package = Package(
// 2
name: "PetSaveOnboarding",
// 3
platforms: [.iOS(.v15), .macOS(.v10_15)],
// 4
products: [
.library(
name: "PetSaveOnboarding",
targets: ["PetSaveOnboarding"]),
],
// 5
dependencies: [],
// 6
targets: [
.target(
name: "PetSaveOnboarding",
resources: [.copy("Resources/Assets.xcassets")]),
]
)
Here’s a breakdown:
- The swift-tools-version:5.5 comment is important as it tells Swift the minimum Swift version required to build this package.
- The name of the Swift package goes here.
- Define the platforms you want your Swift package to work on.
- It defines the library or executables a Swift package produces. It also makes it available to other apps and packages.
- Add any third-party frameworks the Swift package depends on.
- It defines the target of the Swift package. It may also define other test targets or packages this target depends on.
Then, delete the group Tests as it’s not needed.
Adding code and resources
Now, using Finder, replace Sources/PetSaveOnboarding in your package with the PetSaveOnboarding framework.
The package’s project navigator now looks like this:
Remove the file Bundle+Extension.swift since the Swift package contains its own module property. Then, remove PetSaveOnboarding.h because you don’t need it anymore.
To avoid any Mac-related compilation errors, set the build schema as any Any iOS Device or choose any iPhone simulator. Then, build the project.
Super! The Swift package is now ready for publishing.
Publishing the Swift package
Note: To follow the rest of the chapter you’ll need a Github account. If you don’t have one already, create one by going to https://github.com/signup.
Now that you created a Swift Package, it’s time to publish it to GitHub. Log in to your GitHub account or create one if you don’t have one.
Then, create a public or a private repository named PetSaveOnboarding. You’ll see a screen similar to this:
Now follow the steps below:
- Go to the directory that contains your package files using the terminal.
- Execute the following command to add the newly created remote repository as the origin:
git remote add origin https://github.com/<---github-user-name--->/PetSaveOnboarding.git
- Execute the following command to add all those files to the repository, commit and push them:
git add --all
git commit -m "Add package sources"
git push --set-upstream origin main
And it’s done! Congratulations, you published your package to GitHub. Now, it’s time to test if you can consume the package in your project.
Consuming the Swift package
Open the PetSave app with the framework you created earlier. Now, you’ll replace the framework with the published GitHub package.
Select File ▸ Add Packages:
In the search bar, paste the repository link that you created. Then click Add Package.
Soon after, Xcode will verify the Swift package and prompt you with the following:
Click Add Package and voilà!. In the PetSave project ▸ PackageDependencies, you’ll see the name and location of the project like this:
Also, in the Project navigator, you’ll see your package under Package Dependencies. Right-clicking Package Dependencies provides you with options like updating to the latest packages versions later if need be.
Finally, delete the PetSaveOnboarding framework from the project.
Build and run. You’ll see the onboarding screen, this time using Swift packages.
That’s so cool, right? You did a great job.
Key points
- Modularization leads to time and cost savings, reusability and faster build times.
- A framework is an encapsulated and modularized piece of reusable bundle.
- Static frameworks link code at compile time. Dynamic frameworks link code at runtime.
-
@AppStorageis a SwiftUI property wrapper for saving values inUserDefaults. - Swift Packages are repositories that enable developers to create, publish and maintain a package. They are managed using Swift Package Manager (SPM).
- Cocoapods and Carthage are alternatives to SPM, which you can use to create and use libraries.
Where to go from here?
This marks the end of this chapter. You got familiarized and grasped a lot of concepts related to modularization. You learned how modularization can play a vital role in making your overall development faster.
If you want to learn more about creating a framework in iOS with Creating a Framework for iOS tutorial. You can also check out screencasts on Reusable iOS Frameworks and Swift packages. Learn more concepts like local and remote packages with the tutorial on Swift Package Manager.
You can check Apple’s documentation on Swift Packages. Moreover, go to the official Swift Package Manager website to read more about it.
In the next chapter, you’ll learn all about the ins and outs of SwiftUI’s Navigation. You’ll go over all the possible ways you can perform navigation in SwiftUI.
Ready to navigate your way to the next chapter? Vamos! :]