Swift Package Manager Guide – making a modular iOS app (2/2)

8 min

read

In the previous part of the Swift Package Manager Guide, I have taken a closer look at all the latest SPM features, including support for iOS apps. This time, I’m going to take it to the next step and show you how helpful SPM now is for making modular apps.

Are you interested in learning more about the latest powerful features of Swift Package Manager? Before you keep on reading, make sure you are already familiar with the 1st part of my SPM guide.

In this part, I’m going to:

  • divide an app into manageable modules 🤹‍♀️,
  • 🗄️ go over the structure of a single SPM package,
  • analyze the Package.swift manifest file,
  • 🔗add dependencies to a library,
  • 📤 publish a library for an organization’s internal use only.

This scenario works for parts of code you want to wrap into a (Swift) package and separate from the project. It’s not a separate reusable library yet, though. This approach has a couple of benefits:

  • makes the responsibility scope of each and every module very clear,
  • helps you write clear code, promoting the use of namespaces,
  • forces you to explicitly decide which parts of the module can be used outside,
  • makes it easy to replace the module with another one provided the public API remains the same.

A package like this will surely change a lot as the app grows. That’s why I think it’s better to version the code of this module together with the whole app.

Back to our example. Let’s say that I want all the network-related code to be separated as a new module. In order to do this, I’m creating a NetworkStack package. I’m planning to use it only to share simple methods for getting insurance quotes via a public API. Details such as endpoint routes, response serialization etc. will remain hidden in the module.

Creating a package

Before you delve deeper into Swift Package Manager, make sure you are familiar with all the basic terms I’m going to use here. Some of the key concepts include modules, packages, products and dependencies.

Before Xcode 11, you could create SPM packages by running a command (the library type is default, the other being the executable type):

swift package init // or swift package init --type library

By default, the init command creates the structure for the library package.

Now, you can do the same using Xcode 11’s interface – File -> Swift Packages -> Add Package Dependency…

It’s worth it to note that:

  • when you add the library package, you also add it to the app’s workspace,
  • there is no need to tick the “Create Git repository on my Mac” box. The code will be version in the main repository of the project.

Adding a package to the project

The workspace now includes InsuranceApp and the NetworkStack module. To use the newly-created library in the app’s code, I need to add it to the main target (in the General tab of InsuranceApp’s target). Note that I’m choosing the 🏛️NetworkStack product rather than the 📦NetworkStack package.

🥳 Voilà! Now, the app has access to all the public features of the NetworkStack library.

🔎 Show me what you’ve got inside

So far so good. The next step is to add something of value to the newly-created module. The structure of the package’s catalog is clearly defined.

📦 Package.swift – manifest file,

📂 Source/NetworkStack contains source files, 

📂 Tests/NetworkStackTests is for all the tests, 

📄 README.md – take your time, of course… ;), 

📝 .gitignore – SPM makes it quite useful
Let’s start with the Package.swift manifest file.

// swift-tools-version:5.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "NetworkStack",
products: [
// Products define the executables and libraries produced by a package, and make them visible to other
 packages.
.library(
name: "NetworkStack",
targets: ["NetworkStack"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [

// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package 
depends on.
.target(
name: "NetworkStack",
dependencies: []),
.testTarget(
name: "NetworkStackTests",
dependencies: ["NetworkStack"]),
]
)

Isn’t it great that you can write the manifest of a Swift package in Swift? 

“The manifest file, or package manifest, defines the package’s name and its contents using the PackageDescription module. A package has one or more targets. Each target specifies a product and may declare one or more dependencies.”

You can read more about the manifest file in the Swift Package Manager’s documentation. Full documentation of API PackageDescription is available here on GitHub.

Adding dependencies to the module

Since the NetworkStack module will keep me busy enough, I’m going to make things a bit easier by using the Alamofire library.

In order to add this dependency to the package, you need to:

  1. add it in the Package.swift manifest file to the dependency table,
dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", .upToNextMajor(from: "5.0.0-rc.2")),
    ]
  1. add it to the target’s dependencies in the same file,
targets: [
        .target(
            name: "NetworkStack",
            dependencies: ["Alamofire"]), // <-------
        .testTarget(
            name: "NetworkStackTests",
            dependencies: ["NetworkStack"]),
    ]

And that’s it! I know, I keep repeating myself, but I’m really amazed at how much easier things can be with Swift Package Manager. With just this, version 5.0.0-rc.2 of Alamofire appeared on the list of Swift Package Dependencies in Project Navigator. Just like that.

However, when I try to build my project, I get an error:

The package product ‘Alamofire’ requires minimum platform version 10.0 for the iOS platform, but this target supports 8.0. 

Packages can choose to configure the minimum deployment target version for a platform by providing platforms in the Package.swift manifest file.
let package = Package(
name: "NetworkStack",
platforms: [ .iOS(.v10) ],
...

Adding an entry to support iOS 10 and higher resolves the issue – the project builds without any errors. For more details about struct SupportedPlatform, head over to the Swift Package Manager documentation.

Let’s get something real done

How exactly can a module like this be useful in a project? I’m going to show it by adding a public method, which fetches a list of insurance advisors.

 


// NetworkStack.swift
import Foundation
import Alamofire

public struct NetworkStack {
    
    public static func getConsultants(_ completion: @escaping ([Consultant]) -> Void) {
        
        AF.request(Endpoint.consultants.url).response(completionHandler: { response in
            do {
                let result = try response.result.get()
                if let data = result,
                    let consultants = try? JSONDecoder().decode([Consultant].self, from: data) {
                    completion(consultants)
                } else {
                    completion([])
                }
            } catch {
                completion([])
            }
        })
    }
}


// Consultant.swift
import Foundation

public struct Consultant: Codable {
     
    public let name: String
    public let email: String
    public let id: Int
    public let phone: String
    public let website: String
}


// Endpoint.swift
import Foundation

internal enum Endpoint {
    
    case consultants
    
    var url: URL {
        switch self {
        case .consultants: return URL(string: "https://jsonplaceholder.typicode.com/users")!
        }
    }
}

In the main ViewController of the app (I know… but this is not an article about app architecture 🙂 ), I’m importing the NetworkStack library and download the list of insurance advisors. Done.

import UIKit
import NetworkStack

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        NetworkStack.getConsultants { consultants in
            print(consultants)
        }
    }
}

By moving all the code responsible for network requests to a new module, the project has become much clearer. The new namespace allows me to encapsulate many structures and operations. It usually is not a good idea to use models that map 1:1 with the API responses  (as a matter of fact, I can’t think of a good scenario for this…), so I could extend the NetworkStack package with mapping to public models, which only include necessary data in specific formats (dates, URLs etc.).

Declaring private Endpoint enum in the module inside guarantees that making additional requests that do not go through the NetworkStack layer will simply be impossible (without very strong effort, of course!)

And now the best part! I decided to share this extremely useful, well-thought-out and thoroughly tested library with another team. Can Swift Package Manager handle that?

Let’s publish a package

Publishing some code that has been a separate module from the start is far more straightforward than trying to extract a piece of the project’s codebase created in the main target, even if it is conceptually separate. The latter scenario requires tons of self-control and experience in order to maintain a good level of concern separation. Besides, this extracted code will have to be wrapped into some kind of package/library anyway, so it’s much better to do it from the start.

What exactly should you do to publish a package? There are a couple steps to it:

  1. Drag the file from Xcode to your desktop. Make sure to hold the “⌥” key to make a copy.
  2. Open the standalone package in Xcode by double-clicking the Package.swift file.
  3. Put the package under source control by going to Source Control -> Create Git Repositories… Xcode will create a local Git repository and automatically commit the current state of the package all at once.
  4. Tag the master with a current version of the library (e.g. “1.0.0.”).
  5. Create a remote repository and push the package and tag to it. You can also do it via Xcode in the Source Control Navigator tab (CMD+2) if you have configured your GitHub/Bitbucket/GitLab in Xcode (I have gone over the process in the 1st part of the guide).

🥳 Voilà!

You can now import the internally-developed library by going to File -> Swift Packages -> Add Package Dependency… Read the first part to learn more on that, but you know what it means 😈 ♾️, it may be wiser to keep on reading for now👇

Boris Buegling has talked about Creating Swift Packages during this year’s WWDC (ca. 8:00). He includes adding content to README and writing tests. I skipped it since it would only complicate the process, while the library’s code should already have solid test coverage (I already used it in the app after all).

A package in the edit mode

You thought we’re done? Not even close! SPM still has a killer feature up its sleeve! 😄 I’m going to show you how you can edit the source code of your project’s packages in the very same Xcode window.

Let’s get back to the InsuranceCalculator library. Let’s say I need to make a change in how it calculates premiums. First, I want to do this locally and see how the update works in the iOS app, and then I want to update the library so that the macOS app gets it too.

In order to change the reference to InsuranceCalculator for a local version, I need to:

  • clone the library’s repository to my local drive,
  • drag the InsuranceCalculator directory to the workspace.

And that’s pretty much it.

Now I can edit the source files of the package in Xcode, in the very same workspace I’m working with all the time! I can now add the discounts applicable when extending the insurance contract in 📦 InsuranceCalculator/Sources/InsuranceCalculator/InsuranceCalculator.swift. I’m also adding more tests.

How to run tests

I’m launching the tests in the Terminal using the swift test command (while inside the catalog that includes the InsuranceCalculator package, of course).

Use updated

The newly-updated calculatePremiums method is automatically available in the main project. No need for any further configuration. I can now calculate premiums, taking into consideration the renewal discount.

Publishing an update

The next step is to update the InsuranceCalculator library in the repository. I can commit all changes and bump the version to 1.1.0 (tagging the master branch) using Xcode’s Source Control interface. Once pushed, the new version is made accessible to all teams. In order to switch back to InsuranceCalculator from the repository, all I need to do is… remove the reference to the local version.

Note that InsuranceCalculator can again be found in the Swift Package Dependencies section, but this time its in the 1.1.0 version. SPM has fetched the latest version of the dependency in the 1.0.0 < 2.0.0 range.

⚙️ Cloning on a different machine

As I already said, I saved the best for last. So here is another killer feature.

Let’s say a new team member is getting into the project. They clone the repository to their local drive. They click CMD+R. And the magic begins. 

Swift Project Manager is sorting out all the dependencies and their versions (sometimes with the help of the InsuranceApp.xcworkspace/xcshareddata/swiftpm/Package.resolved file, provided it’s present in the repository), fetching and checking them out. Next, Xcode is building the project, adding all the required libraries in the process. No extra commands, no need to configure any paths. Everything simply works as it’s supposed to.  🙇

Summary

For me, this series of articles is a way to express my admiration and gratitude for the new and improved Swift Package Manager, which now supports all platforms within the Apple ecosystem and easily integrates with Xcode 11. In this part, I focused on creating, maintaining and distribution of your own packages. It’s another big benefit of using SPM, since the easier it is to build and maintain a modular app, the easier it is to invest your time and effort in making one. I really hope that it will all translate into much clearer and pleasant codebases you gladly return to months or even years later. 🤞Also, if you want to learn even more about trends in Swift, make sure to read my previous article on protocol-oriented programming in Swift.

Estimate your project





or contact us directly at [email protected]

Thanks

Thank you!

Your message has been sent. We’ll get back to you in 24 hours.

Back to page
24h

We’ll get back to you in 24 hours

to address your needs as quick as possible.

Estimation

We’ll prepare an estimation of the project

describing the team compostition, timeline and costs.

Code review

We’ll perform a free code review

if you already have an existing system or a part of it.

Our work was featured in:

Tech Crunch
Forbes
Business Insider

Aplikujesz do

The Software House

Aplikuj teraz

wyślij CV na adres: [email protected]

CopiedTekst skopiowany!

Nie zapomnij dodać klauzuli:

Kopiuj do schowka Copy

Jakie będą kolejne kroki?

Phone

Rozmowa telefoniczna

Krótka rozmowa o twoim doświadczeniu,
umiejętnościach i oczekiwaniach.

Test task

Zadanie testowe

Praktyczne zadanie sprawdzające dokładnie
poziom twoich umiejętności.

Meeting

Spotkanie w biurze

Rozmowa w biurze The Software House,
pozwalająca nam się lepiej poznać.

Response 200

Response 200

Ostateczna odpowiedź i propozycja
finansowa (w ciągu kilku dni od spotkania).

spinner