SPM command-line interface

In the preceding articles, we’ve been using Xcode as a tool for creating, developing, and fetching Swift packages. At the end of the day, the program doing the heavy job is SPM. Xcode interfaces with it, providing a nice GUI.

In this article, we’ll take a closer look at SPM itself, an independent program belonging to the Swift project. We’ll explore its main commands by building and releasing a trivial swift executable.

Main commands

If we type man swift in the terminal, it’ll display the manual page for Swift:

man page for the Swift command.

As we can see, the commands for controlling SPM are:

  • build
  • test
  • run
  • package and its subcommands

If we type swift package in the terminal, it lists the following sub-commands (notice how many they are, as opposed to the three commands Xcode provides):

The subcommands of swift package

Creating an executable

Besides libraries, a Swift Package can produce executables too. This is how the Swift project creates its command-line programs (e.g: docc, swift-driver). To create an executable package, we need to type the following in the terminal (I’m using ZSH):

mkdir simple-executable
cd simple-executable
swift package init --name "Simple Executable" --type executable

# If you wish to see other options available, type `swift package --help`

This will initialize a package with an executable target:

swift package init output.

Here’s what the manifest looks like:

// swift-tools-version:5.5

import PackageDescription

let package = Package(
    name: "Simple Executable",
    dependencies: [],
    targets: [
        .executableTarget(
            name: "Simple Executable",
            dependencies: []),
        .testTarget(
            name: "Simple ExecutableTests",
            dependencies: ["Simple Executable"]),
    ]
)

Building

To build our package, we use the swift build command:

Building the package.

Use the swift package clean command to clear the build caches.

Running

Running the executable is simple, simply type swift run:

Running the executable.

Resolving & updating dependencies

Let’s first include a dependency in our manifest:

let package = Package(
    // ...
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"),
    ],
    targets: [
        .executableTarget(
            name: "Simple Executable",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser")
            ]),
    // ...
    ]
)

With this dependency now declared, we can use the following commands:

  • swift package resolve to resolve the dependencies
  • swift package show-dependencies to show the currently resolved dependencies
  • swift package update to update the dependencies
  • swift package reset to reset the cache and build directories
Commands for handling dependencies.

Releasing

Unfortunately, the swift package command doesn’t explain how we can build an executable for release. You can find this information in the GitHub repository. Here’s how it works:

  1. Use swift build -c release to build the executable using the release configuration
  2. The built executable lives inside the .build/release folder. We can invoke it directly from there: type .build/release/Simple\ Package to see what happens.
Building for release and running the executable.

Invoking it from ZSH

We can include the executable in one of the folders used in the $PATH variable. This allows ZSH to find and run the executable we created. Type the following in your terminal:

  1. echo $PATH, this will display the folders separated by colon characters (:)
  2. Rename the executable to an invokable name:
    1. mv .build/release/Simple\ Executable ./say-hello
    2. If /usr/local/bin doesn’t exist, create it with this command: sudo mkdir /usr/local/bin
    3. sudo mv ./say-hello /usr/local/bin
  3. Now we can simply invoke our command by typing say-hello
Invoking the executable using the terminal.

Conclusion

SPM is a powerful program capable of managing dependencies and building libraries and executables. It is a key component of the Swift project, supporting the usage of the language in different operating systems.

In this article, we saw how we can create a trivial package that builds an executable. We also explored the main SPM commands, and how we can invoke our executables directly from the terminal’s $PATH variable.

Semantic versioning

In the previous article, we talked about how we can publish a swift package, and how we version it using semantic git tags. But what does semantic mean? In this article, we’ll explore this subject in more detail.

The basics

A library’s public API is all the symbols (e.g: types, variables, and functions) used by its clients via import. When we depend on a library, we also depend on its public API to accomplish a task. This dependency makes part of our code coupled to it.

Imagine we’re using a library from a popular Swift package. What would happen if the maintainers released a new version with huge changes to its public API? Our code would break. To avoid this issue, we could rely on a single version. But this means we would be locked in it, incapable of updating the package without updating our code. To make things worse, imagine each package owner has a different way of handling versions. These scenarios are what we call dependency hell.

Semantic versioning is a specification aimed at putting an end to dependency hell. It divides versions into three numbers: major, minor, and patch (e.g. 1.5.8). Here’s what each number mean:

  • Major means the public API has changed, requiring our code to be updated
  • Minor means new code was added to the public API, the additions don’t break existing integrations (backward compatible)
  • Patch means bugs were fixed without changing the public API

Selecting package versions

When adding a Swift package, the recommended approach is to select a version using the up to next major dependency rule:

Use the Up to Next Major version.

It means our project we’ll receive updates that don’t break our code. Select the Up to next minor rule if you only wish to receive bug fixes.

When declaring dependencies on the package, we can also specify those same version rules:

// ...
dependencies: [
    .package(
        url: "https://github.com/apple/swift-algorithms.git",
        .upToNextMajor(from: "1.0.0")
    )
],
// ...

Guidelines

Besides defining how to organize our versions, the specification also comes with some important guidelines:

Development versions (below 1.0.0)

Every version below 1.0.0 is considered unstable and under development. The public API isn’t defined yet and can change drastically.

The initial version (1.0.0)

Once the public API is defined, it’s time to release the initial version of the package. The public API is considered stable and set in stone.

Never modify a released version

If you wish to correct something, release a new version on top of the one with errors.

Use a development branch

You don’t need to release every single change you make. We can release multiple changes using a single version. Having a development branch allows developers to decide when to release a batch of changes as a single major, minor, or patch.

Pre-releases

We can also pre-release versions. This is useful for testing features as they get added. They follow the same versioning rules but have a specific format in the end:

1.2.5-beta1
1.2.5-1
1.2.5-alpha-633

A pre-release doesn’t need to be stable. Use it only to test a set of features while they are being developed. Pre-releases have less precedence over official versions:

1.2.5 > 1.2.5-beta1

Usage with SPM

On the package side, to specify a pre-release, we use the Version type:

// ...
dependencies: [
    .package(
        url: "https://github.com/apple/swift-algorithms.git",
        from: Version(1, 2, 5, prereleaseIdentifiers: ["alpha", "1"]))
],
// ...

The above code specifies a range of versions: 1.2.5-alpha-1 ..< 2.0.0.

On the project side (using Xcode 13.2.1), we can include the pre-release identifiers directly:

1.2.0-beta.1 ..< 2.0.0

If we include the pre-release identifiers, SPM will fetch the versions automatically when we update our packages.

Keep a changelog

It’s important to document every version being added. The documentation is added to a file called CHANGELOG.md. It contains the history of your project, with relevant information on what was added, removed, or changed. Here’s an example from the Swift-Algorithms package.

For more information, visit this link.

Closing thoughts

If you wish to correctly use dependency managers, make sure to understand what semantic versioning is. We’ve explored what problems it solves, and also some of its main guidelines. We also understood what a CHANGELOG file is, and how we can use it.

In the next article, we’ll talk about building executables with SPM. We’ll also take a look at its command-line interface.

Publishing a Swift Package

This article is the third post in a series about SPM. If you don’t know what a Swift Package is, I recommend you to take a look at the first and second articles. Today we’ll explore how we can publish and version a package.

Centralized vs. Decentralized

Dependency managers can have two different strategies for organizing and fetching their packages. They can be centralized or decentralized.

A centralized dependency manager has a central place where all packages are located. It then uses this place to fetch a dependency. The advantage of this approach is that it offers easy discoverability (e.g: a website we can use to search for dependencies).

A decentralized dependency manager doesn’t have a specific place containing all registered packages. It allows dependencies to be published independently, requiring us to inform their URL to fetch them. The advantage of this approach is that it doesn’t rely on a single source, which makes it more flexible and resilient to failures.

Git usage

SPM is decentralized, and makes heavy use of Git. Xcode offers an option to initialize a local Git repository when we create a package:

You can initialize the git repository when creating a package.

It also adds an initial commit for us:

The initial commit Xcode added for us.

The default folder structure even comes with the README.md and .gitignore files (notice we also have the .git folder for our local repository):

The listed files under our package folder.

Creating a remote repository

To publish a package, all we need to do is create a remote Git repository and push our local package commits to it. SPM will then be able to fetch the package. We’ll be using Github, but we can publish a package to other Git servers as well.

When creating the repository, make sure you mark it as public and that it’s empty (doesn’t include a README.md or license):

Creating a repository on Github.

Once the remote repository is created, we need to configure the remote inside our package:

Configuring a remote.

After we push our local commits, we can use the repository URL to fetch the package:

Fetching our package.

Private packages

It’s pretty common to develop and use private packages. They belong to a person or organization, and are shared internally with multiple apps or packages. Their publication process is the same, but the repository is private.

To fetch private packages, we need to configure how we authenticate to the Git server we’re using. SPM will then be able to access our private repositories. There are two common protocols we might use: HTTPS and SSH.

To setup authentication, we first need to configure a Github account in the preferences panel (preferences -> Accounts). Xcode will require a personal access token. To get one, follow the steps in this link.

Accounts panel: preferences -> accounts

HTTPS

Authenticating with HTTPS is simple. Once we have the account in place, we need to ensure it will clone the repositories using this protocol:

A Github account using HTTPS.

SSH

SSH is more complicated to be configured. This link explains it in detail. When creating the keys, make sure you don’t use the ssh-ed25519 encryption algorithm, because it’s unsupported (we need to use RSA instead):

Don’t use the ed25519 algorithm.
Use RSA instead. The instructions are in this link.

Once we have SSH setup, we need to configure our Xcode account to use our private key. Notice that we also need to clone using SSH:

Configuring the account to use ssh.

Versioning

Once we publish our package, we need to ensure its clients receive updates. SPM uses git tags to mark the versions of a package, and we use semantic versioning to organize them. We release a new version when we want the clients to receive a change. It might be a bug fix or performance improvement, for example.

The workflow for releasing a new version is pretty simple:

# To list our versions:
git tag

# To create a local version:
git tag -a major.minor.patch -m "tag description"

# To publish a version:
git push origin major.minor.patch

# To remove a version already published:
git push origin --delete major.minor.patch
git tag -d major.minor.patch

To summarize

  • SPM makes heavy use of Git
  • We can initialize a Git repository when creating a package
  • To publish a package, push its local changes to a remote repository
  • To fetch private repositories, configure the Xcode account with either HTTPS or SSH
  • Use git tags to manage the versions of a package

Versioning packages is an important topic that deserves attention. In the upcoming article, we’ll look into it in more detail.

The structure of a Swift Package

In the previous article, we talked about what SPM is and how we can use it. We briefly talked about packages, but we are missing a key point: how to create them. In this article, we’ll explore packages in more detail. If you don’t know what a package is, I recommend you first check the previous blog post before continuing.

Creating a package

We can use Xcode to create packages (in this article I’ll be using version 13.2.1). With Xcode running, Let’s go to File -> New -> Package:

Selecting the package creation menu.

Selecting this option will open the following window:

Package creation screen in Xcode.

Once we are good with the location and name of our package, we can click the create button, and SPM will create our Swift package with a default folder structure.

Folder structure

The folder structure of our package.

When created, a package comes with a default folder structure. Each file and folder has a purpose:

  • README.md: Describes the package to humans
  • Package.swift: A manifest file defining what the package is
  • Sources: Folder containing the source files of our package
  • Tests: Folder containing the unit test suites covering the code from the Sources folder

The manifest file

Every Swift package has a manifest (named Package.swift) identifying it. A manifest contains important information about a package:

  • Its name
  • The platforms it supports
  • The targets it consists of
  • Its dependencies
  • The products it distributes (libraries)

SPM uses this information to manage our packages. The following code comes from our Package.swift file:

// 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(
    name: "Simple-Package",
    products: [
        .library(
            name: "Simple-Package",
            targets: ["Simple-Package"]),
    ],
    dependencies: [
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "Simple-Package",
            dependencies: []),
        .testTarget(
            name: "Simple-PackageTests",
            dependencies: ["Simple-Package"]),
    ]
)

Let’s understand the most important attributes of a package:

Targets

A package is divided into targets. They are the building blocks of a package. Each target has a specific purpose and defines its module:

targets: [
    .target(name: "Simple-Package", dependencies: []),
    // More targets can be defined here.
]

Targets have their own folder under Sources, containing their source code. They can also depend on external code or on other targets as well. This allows us to modularize our code if needed.

Test targets

We can write and run automated tests for the code contained in a target. All we have to do is define a test target with the corresponding folder and test suites (notice how it uses our Simple-Package as a dependency):

targets: [
    // ...
    .testTarget(name: "Simple-PackageTests", dependencies: ["Simple-Package"])
]

Products

The module defined by a target isn’t directly accessible to the clients of a package. It is initially internal. The only way to distribute the public API of a target is to define a product of the library type. It vends the module defined by its target, allowing the clients to use its public API via import:

products: [
    .library(name: "Simple-Package", targets: ["Simple-Package"]),
]

Dependencies

Packages can also depend on other packages. When we declare dependencies, we specify which versions we want:

dependencies: [
    .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
]

Right after we edit our manifest, SPM will resolve our dependencies, the same process it does on a regular Xcode project:

The resolved dependencies (swift-algorithms depends on swift-numerics)

We can then use any products from swift-algorithms in our targets:

/// ...
.target(
    name: "Simple Package", 
    dependencies: [
        .product(name: "Algorithms", package: "swift-algorithms")
    ]
),
/// ...

Workflow

The workflow for packages using Xcode is pretty similar to the one for regular projects (e.g iOS or MacOS).

Adding files

To add swift files to a target, simply place them inside its folder. This file will then belong to the target’s module, which means we can add new code to it and access the other code already defined in there.

Adding a file to the `Simple Package` target.

Building and testing

Building is pretty simple. Just select the product from the list and build it. I’ve added some targets and products to exemplify:

Selecting a product to be built.

Here’s how the manifest looks like:

// 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(
    name: "Simple Package",
    products: [
        .library(
            name: "Simple Package",
            targets: ["Simple Package"]),
        .library(
            name: "Library1",
            targets: ["Target1"]),
        .library(
            name: "Library2",
            targets: ["Target2"]),
        .library(
            name: "Library3",
            targets: ["Target3"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "Simple Package",
            dependencies: [
                .product(name: "Algorithms", package: "swift-algorithms")
            ]),
        .target(
            name: "Target1",
            dependencies: []),
        .target(
            name: "Target2",
            dependencies: []),
        .target(
            name: "Target3",
            dependencies: []),
        .testTarget(
            name: "Simple PackageTests",
            dependencies: ["Simple Package"]),
    ]
)

We also have to define the folders for each target:

The new folder structure in Sources.

Conclusion

We’ve learned how to build packages and what they look like in terms of structure. We explored what manifests are and how we can use them to define a package. We also looked at the key components a package has:

  • Dependencies
  • Targets
  • Products

The package we built doesn’t have any source code, just meaningless files. I encourage you to look at real-world examples, like the Time package we used in the previous post or the Algorithms package we used in this one.

In the next article, we’ll take a look at how we can publish and version our packages.

The basics of Swift Package Manager

Most of the code we write depends on external libraries. Libraries are pieces of code that can be used by many applications. The OS already comes with essential libraries we can use as building blocks, and all we have to do is import their modules. This is what we do when we need libraries like Foundation, SwiftUI, or UIKit. But what happens when we need to use a library that doesn’t come with the OS?

In this case, we can manually download the library and embed it in our project. This process gets complicated if we depend on multiple libraries, though. A library might also depend on other libraries, which means we would need to manually manage each sub dependency. Another problem we might face is dealing with versions. How do we know when to update a specific dependency?

Those problems are what make dependency managers exist. They automate those processes, making our lives simpler and allowing us to focus on writing code. SPM is Apple’s dependency manager and is part of the Swift project. It integrates with the Swift build system to provide us the full experience of downloading, compiling, linking, and updating dependencies. Xcode also integrates with it, exposing controls to use this tool.

Packages

SPM works by managing packages. A package is a flexible container of source code that can distribute libraries to clients. In this article, we’ll be using a simple open-source package called Time. More specifically, we’ll explore how we can use SPM to use this library in our code.

The examples in this article use Xcode 13.2.1. If you wish to follow along, create a new iOS project using Swift (the UI framework doesn’t matter).

Declaring Dependencies

To use a package in our iOS app, we first need to declare it as a dependency in the Package Dependencies screen:

  1. In the navigation area, select the project
  2. At the left bar, select the project again (not the target)
  3. At the top tab bar, select the Package Dependencies tab
The Package Dependencies screen on Xcode

Now we need to add the Time package:

  1. Copy the repository URL: https://github.com/davedelong/time.git
  2. Select the + button, under Packages
  3. Paste the copied URL in the search bar at the top
  4. In the dependency rule option, select “Up to Next Major Version”
Adding Time as a package

Xcode will then fetch this package and ask us which library we want to use (one package can distribute multiple libraries). This package is simple, it only distributes one:

Adding the Time product

Once we add this package to our project, Xcode will display it in the Package Dependencies list, in the navigation area:

The list of resolved packages in the navigation area

Package Resolution

After we declare the dependencies in our project, SPM will resolve them:

  1. Look at the URL and version of each package
  2. Fetch each dependency with the correct version
  3. Repeat this process for each sub dependency of a package if it has any
  4. Configure the packages with the project, allowing us to import their distributed libraries

After the package resolution phase, SPM generates a JSON file called Package.resolved. It contains which packages were resolved and what their versions are. SPM uses it to figure out whether a dependency needs to be updated or not. Here’s what this file looks like:

{
  "object": {
    "pins": [
      {
        "package": "Time",
        "repositoryURL": "https://github.com/davedelong/time.git",
        "state": {
          "branch": null,
          "revision": "be6cbbbb97aa4570e3b51bd56f98ca3cf62aa3cb",
          "version": "0.9.2"
        }
      }
    ]
  },
  "version": 1
}

Package.resolved is located in the swiftpm folder, inside your xcodeproj file:

SPM-Basics.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

It’s a good idea to put the swiftpm folder under version control. This way we can ensure every team member is using the right package versions.

Packages Location

The resolved packages are located in the derived data folder. A lot of times developers have problems with Xcode, and deleting this folder sometimes solves those issues. Just be aware that when you delete it, you’ll also delete the cached dependencies, forcing a new resolution of packages.

The Three SPM Actions

Once we have the packages in place, we can use three different actions for dealing with them:

Actions for dealing with SPM

Reset Package Caches

Use this action to erase the cached packages of our project from the derived data folder. SPM will automatically resolve the dependencies again.

Resolve Package Versions

Use this action to force a new resolution of dependencies. If your package.resolved file was updated by another team member, this action will ensure your local packages match the versions in that file.

Update to Latest Package Versions

The action name says it all. It will check if any dependency has available updates and will update them according to the dependency rule (e.g Up to next major). Semantic versioning is used to organize the package versions.

Be careful

Packages can come from third-party developers. Before using these packages, ensure the following:

  1. You understand what the package does and what sub dependencies it uses
  2. It comes from a trusted source
  3. If it’s an open-source package, ensure it’s maintained by the community (check the latest changes, number of stars, and so on)
  4. The package license allows your application to use it

Conclusion

In this article, we’ve explored how SPM works. Here’s what we learned:

  • What SPM and packages are
  • How to add packages to an Xcode project
  • What package resolution is
  • Where Xcode stores the resolved packages
  • How to manage the packages in Xcode
  • What to take into account when adding a package

In the upcoming article, we’ll explore how we can create a Swift package and what it looks like in terms of structure.

My thoughts on TDD

A natural workflow for software developers is to code and then conduct some manual tests to make sure things are working correctly. While this approach works, it also has some issues:

  • It’s error-prone (the tests are manually conducted)
  • We normally only test a small portion of the app (the part under development)

To be fair, manually testing only the part under development is not the problem. The problem is that we normally don’t conduct all previous tests for the other features. We might introduce a breaking change without noticing it.

Beyond manual tests, there are other tools we can use to ensure the correctness of our code. One of them is unit tests. They ensure each component in our code performs correctly in isolation, increasing the overall quality of the software we write.

TDD is a workflow that builds on top of automated tests in general. I don’t think it works in every scenario, but when used with unit tests this technique shines:

  • It gives us instant and reliable feedback on the changes we make
  • It disciplines us to write tests covering the features we develop
  • We are informed if we break an existing functionality

I’ve used TDD in the second and third assignments of the CS193P course. These assignments ask the students to build two card games for iOS. I began the development by the model (which contains the game logic), and I didn’t need to write the UI layer to only then manually test the business logic. Having the tests in place also gave me confidence that it worked. Here is a commit that shows this workflow in action (Each feature has some related tests ensuring it works).

TDD Cons

It’s hard to use this technique if:

  • We don’t have a good understanding of the project
  • We have a tight schedule
  • We are writing UI or integration tests (those take time to write and run and might give different results)

Many of TDD’s benefits come from the fact that it enforces us to write automated tests. If we get into the habit of always writing tests for the changes we make, we are also in a good shape.

Notification content extensions and Core Data

The UserNotifications framework introduced in iOS 10 is really powerful. I’ve been using it lately since my app makes use of local notifications to remind users about their habits. I didn’t know, however, that there was the possibility to display a custom ViewController when the notifications were focused. Doing so is part of an app extension called Notification Content Extension.

What the app content extension displays

I wanted to display the progress of the habits associated with the notifications. Each habit might have a challenge of days, which is displayed to the user, showing its current day, how many days to finish the challenge, and how many days were missed and executed. The habit can also be a daily one, which means there’s no current challenge of days. In this case, the ViewController only shows how many days were executed.

Simulator Screen Shot - iPhone 7 Plus - 2018-12-04 at 07.00.17

Accessing the data of each habit from the extension

Habit Calendar, the app I’m building, uses Core Data as the Model layer. The problem is that extensions aren’t apps. They are only a portion of your app, and I couldn’t access my core data stack initialized in the AppDelegate from it.

To solve this issue, I had to do 4 things:

  1. Create an app group and share it with the app and the extension.
  2. Move my data store file (I’m using the SQLite store type) to the shared container of the app group. This was needed because my App was already in the AppStore.
  3. Use the new location in my DataController (the object holding the whole CoreData stack).
  4. Initialize it within the ViewController of the extension, and retrieve/modify the data.

The first step is described in this post, however, nowadays it can be done entirely from Xcode.

To move my store file, I had to do the following (this code is executed before loading the data stores, in my DataController class):

    /// Shares the storage file with the app group (only in cases of an app update, before adding extensions).
    private func shareStoreIfNecessary() throws {
        let fileManager = FileManager.default

        // Get the default store url (when it's not shared with the app group).
        let previousStoreUrl = try? fileManager.url(
            for: .applicationSupportDirectory,
            in: .userDomainMask,
            appropriateFor: nil,
            create: false
        ).appendingPathComponent("HabitCalendar")

        // Get the store url when it's shared with the app group.
        let appGroupStoreUrl = fileManager.containerURL(
            forSecurityApplicationGroupIdentifier: "group.tiago.maia.Habit-Calendar"
        )?.appendingPathComponent("HabitCalendar")

        // Share the file, if not yet shared and only in cases of an app update before the adition of app extensions.
        if let previousStoreUrl = previousStoreUrl, let appGroupStoreUrl = appGroupStoreUrl {
            if fileManager.fileExists(atPath: previousStoreUrl.path),
                !fileManager.fileExists(atPath: appGroupStoreUrl.path) {
                do {
                    print("Moving store from private bundle to shared app group container.")
                    try fileManager.moveItem(
                        at: previousStoreUrl,
                        to: appGroupStoreUrl
                    )
                } catch {
                    assertionFailure("Couldn't share the store with the extensions.")
                    throw error
                }
            }
        }
    }

Now it’s possible to load the core data stack in the extension and retrieve the necessary data for display:

    func didReceive(_ notification: UNNotification) {
        guard let habitID = notification.request.content.userInfo["habitIdentifier"] as? String else {
            assertionFailure("The notification request must inform the habit id.")
            return
        }
        habitNameLabel.text = notification.request.content.title

        dataController = DataController { [weak self] error, persistentContainer in
            if error == nil {
                // Fetch the habit and display its data.
                let request: NSFetchRequest = HabitMO.fetchRequest()
                request.predicate = NSPredicate(format: "id = %@", habitID)

                guard let result = try? persistentContainer.viewContext.fetch(request), result.count > 0 else {
                    assertionFailure("""
Trying to display the habit but it doesn't exist anymore. Any scheduled notifications should've been removed.
"""
                    )
                    return
                }

                self?.habit = result.first
                self?.display(result.first!)
            }
        }
    }

Useful links

 

Persistent Image Gallery (CS193p fall of 2017, assignment VI solution)

The assignment VI  asked the student to convert the ImageGallery app from the previous assignment (V) and make it support the new Files API, using a UIDocumentBrowserViewController instance to present the documents.

I enjoyed the new Apple’s API and solution to deal with documents in iOS. Now an app can add integration to the new Files app, the documents you interact with now provide autosave, asynchronous open and close operations and they also take care of storage for you (filesystem, iCloud). Not to mention the new UIDocumentBrowserViewController class, which handles all common document operations just like the Files app does, and provides a similar interface. You can also export your own type, marking your app as the owner and editor.

This assignment also asked the student to provide caching for the image fetching requests. I’ve done so using the URLCache class and providing it to my own URLSessionConfiguration.

Main challenges

To deal with documents I had to create my own instance of the UIDocument class, which handles an instance of my ImageGallery struct. I had already written the code necessary to convert the model to JSON, pretty simple with the new Codable API.

class ImageGalleryDocument: UIDocument {
  
  // MARK: - Properties
  
  /// The document thumbnail.
  var thumbnail: UIImage?
  
  /// The gallery stored by this document.
  var gallery: ImageGallery?
  
  // MARK: - Life cycle
  
  override func contents(forType typeName: String) throws -> Any {
    return gallery?.json ?? Data()
  }
  
  override func load(fromContents contents: Any, ofType typeName: String?) throws {
    if let data = contents as? Data {
      gallery = ImageGallery(json: data)
    }
  }
  
  override func fileAttributesToWrite(to url: URL, for saveOperation: UIDocumentSaveOperation) throws -> [AnyHashable : Any] {
    var attributes = try super.fileAttributesToWrite(to: url, for: saveOperation)
    if let thumbnail = thumbnail {
      attributes[URLResourceKey.thumbnailDictionaryKey] = [URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey : thumbnail]
    }
    
    return attributes
  }
}

In the gallery display controller, I’ve added properties to hold the new document. All I had to do was to open, change and save it.

  // ...
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    galleryDocument?.open { success in
      if success {
        if self.gallery == nil {
          self.gallery = self.galleryDocument!.gallery
          self.gallery.title = self.galleryDocument!.localizedName
        }
      } else {
        self.presentWarningWith(title: "Error", message: "Document can't be viewed.") {
          self.dismiss(animated: true)
        }
      }
    }
  }

  // ...

  @IBAction func didTapDone(_ sender: UIBarButtonItem) {
    galleryDocument?.gallery = gallery
    
    if !cachedImages.isEmpty {
      galleryDocument?.thumbnail = cachedImages.first?.value
    }
    
    galleryDocument?.updateChangeCount(.done)
    galleryDocument?.close() { success in
      if !success {
        self.presentWarningWith(title: "Error", message: "The document can't be saved.") {
          self.dismiss(animated: true)
        }
      } else {
        self.dismiss(animated: true)
      }
    }
  }

To provide support for caching, I’ve created a class handling all image fetching requests. Inside this class, I’ve configured a specific URLSession.

/// The session used to make each data task.
private(set) lazy var session: URLSession = {
  let cache = URLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 80 * 1024 * 1024, diskPath: nil)
    
  let configuration = URLSessionConfiguration.default
  configuration.urlCache = cache
  configuration.requestCachePolicy = .returnCacheDataElseLoad
    
  return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil)
}()
  
// MARK: - Imperatives
  
/// Requests an image at the provided URL.
func request(
  at url: URL,
  withCompletionHandler completion: @escaping (Data) -> (),
  andErrorHandler onError: @escaping (Error?, URLResponse?) -> ()
) {
  let task = session.dataTask(with: url) { (data, response, transportError) in
    guard transportError == nil, let data = data else {
      onError(transportError, nil)
      return
    }
    
    guard let httpResponse = response as? HTTPURLResponse,
      (200...299).contains(httpResponse.statusCode),
      ["image/jpeg", "image/png"].contains(httpResponse.mimeType) else {
        onError(nil, response)
        return
    }
    completion(data)
  }
  task.resume()
}

The source code for this solution can be found in my Github repository.

Image Gallery (CS193p fall of 2017, assignment V solution)

The assignment V asked the student to create an iPad app which let users drag images from other apps (using multitasking) and create an image gallery from them. Each dragged image needed to be fetched from its URL in order to be presented in the gallery’s collection view. The image galleries can be removed, edited or added from a master controller, which presents each gallery document in a table view. Each image can be displayed in a separate detail controller, which uses a scroll view and an image view to display the selected image.

The main features to be learned were:

  • Table views (to display each gallery as a document)
  • Collection views (to display each image)
  • multithreading (involved in requesting the image’s data)
  • Scroll views (used to display the images in detail, using an Image view as the subview)
  • Text fields (used to rename a gallery from its table view cell)
  • Drag and drop API

Main challenges

The most difficult feature to be added was to support the drop interactions in the gallery’s collection view. I had some problems to correctly add the image to the data source while dismissing the placeholder collection view cell. The drop interactions also involved fetching the image from the loaded URL.

...
// Loads the URL.
_ = item.dragItem.itemProvider.loadObject(ofClass: URL.self) { (provider, error) in
  if let url = provider?.imageURL {
    draggedImage.imagePath = url

    // Downloads the image from the fetched url.
    URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
      DispatchQueue.main.async {
        if let data = data, let _ = UIImage(data: data) {
          // Adds the image to the data source.
          placeholderContext.commitInsertion { indexPath in
            draggedImage.imageData = data
            self.insertImage(draggedImage, at: indexPath)
          }
        } else {
          // There was an error. Remove the placeholder.
          placeholderContext.deletePlaceholder()
        }
      }
    }.resume()
  }
}

I also had some difficulties in handling the drop from within the app (reordering of images). Hopefully, I had the demo code, which had a similar situation and treatment:

func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool {
  if collectionView.hasActiveDrag {
    // if the drag is from this collection view, the image isn't needed.
    return session.canLoadObjects(ofClass: URL.self)
  } else {
    return session.canLoadObjects(ofClass: URL.self) && session.canLoadObjects(ofClass: UIImage.self)
  }
}
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
  guard gallery != nil else {
    return UICollectionViewDropProposal(operation: .forbidden)
  }

  // Determines if the drag was initiated from this app, in case of reordering.
  let isDragFromThisApp = (session.localDragSession?.localContext as? UICollectionView) == collectionView
  return UICollectionViewDropProposal(operation: isDragFromThisApp ? .move : .copy, intent: .insertAtDestinationIndexPath)
}

And in the perform drop method:

func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
  let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)

  for item in coordinator.items {
    if let sourceIndexPath = item.sourceIndexPath {

      // The drag was initiated from this collection view.
      if let galleryImage = item.dragItem.localObject as? ImageGallery.Image {

        collectionView.performBatchUpdates({
          self.gallery.images.remove(at: sourceIndexPath.item)
          self.gallery.images.insert(galleryImage, at: destinationIndexPath.item)
          collectionView.deleteItems(at: [sourceIndexPath])
          collectionView.insertItems(at: [destinationIndexPath])
        })

        coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
      }   
    } else {
      // The drag was initiated from outside of the app.
      ...
    }
  }
}

I’ve completed all the extra credit tasks for this assignment. I’ve added a new class called ImageGalleryStore, which was in charge of persisting the gallery models using the UserDefaults API. The storage mechanism used the NotificationCenter API to communicate the model changes back to the controllers. To make each gallery model become able to be stored I’ve simply added conformance to the Codable protocol.

I’ve also added support to the deletion of images using a drop interaction in a UIBarButtonItem. Since UIBarButtonItems aren’t views (and can’t handle drop interactions because of that), I had to use the custom view initializer, adding a button with the drop interaction attached to it:

override func viewDidLoad() {
  super.viewDidLoad()
  // In order to implement the drop interaction in the navigation bar button,
  // a custom view had to be added, since a UIBarButtonItem is not a
  // view and doesn't handle interactions.

  let trashButton = UIButton()
  trashButton.setImage(UIImage(named: "icon_trash"), for: .normal)  

  let dropInteraction = UIDropInteraction(delegate: self)
  trashButton.addInteraction(dropInteraction)

  let barItem = UIBarButtonItem(customView: trashButton)
  navigationItem.rightBarButtonItem = barItem  

  barItem.customView!.widthAnchor.constraint(equalToConstant: 25).isActive = true
  barItem.customView!.heightAnchor.constraint(equalToConstant: 25).isActive = true
  
  ...
}

The source code for this solution can be found in my Github repository.

Animated Set (CS193p fall of 2017, assignment IV solution)

This project is comprised of two parts: A Set game and a Concentration game, both presented by a UITabBarViewController. The Set game written in this app is a game based on the Set one, which is a popular card game (I’ve used this video to get its business logic). The Concentration is a simple game based on matches of card pairs, which are then removed from the table. All these projects were developed based on the previous assignments (I, II and III).

The fourth assignment required the student to add animations to the following events of the Set game:

  • All cards are smoothly rearranged in the grid, this is done using UIViewPropertyAnimator to animate each card’s frame.
  • All cards are dealt one by one at the beginning of the game and after the discovery of a match. Each card moves from the deck to its appropriate position in the cards grid. As per the requirements, two cards mustn’t be dealt at the same time. The animation uses UIViewPropertyAnimator combined with the UIDynamicAnimator class and a UISnapBehavior.
  • In the case of a match, cards are removed from the grid and put into the matched deck. This removal animation was also written using the UIViewPropertyAnimator, UIDynamicAnimator and UISnapBehavior classes.
  • The flipping over of cards. This animation was written with the transition API in the UIView class.
The usage of a UITabBarController to embed the Set and Concentration controllers was also required, and the Concentration tab also had to use a UISplitViewController, with a theme chooser as the master controller and the game’s controller as the detail one.
 Assignment IV project storyboard

Main challenges

I had some troubles before getting the deal animation working fine, it conflicted with the rearrangement of cards, which uses a UIViewPropertyAnimator to animate each card’s frame to its correct position in the grid. This happened mainly because of both animations modifying the same properties at the same time:

override func layoutSubviews() {
  super.layoutSubviews()
    
  // Only updates the buttons frames if the centered rect has changed,
  // This will occur when orientation changes.
  // This check will prevent frame changes while
  // the dynamic animator is doing it's job.
  if grid.frame != gridRect {
    updateViewsFrames()
  }
}

The deal animation can be triggered by the addition of new cards to the container and can also be independently called by using the dealCardsWithAnimation method in the container view:

/// Adds new buttons to the UI.
/// - Parameter byAmount: The number of buttons to be added.
/// - Parameter animated: Bool indicating if the addition should be animated.
func addButtons(byAmount numberOfButtons: Int = 3, animated: Bool = false) {
  guard isPerformingDealAnimation == false else { return }
    
  let cardButtons = makeButtons(byAmount: numberOfButtons)
    
  for button in cardButtons {
    // Each button is hidden and face down by default.
    button.alpha = 0
    button.isFaceUp = false
      
    addSubview(button)
    buttons.append(button)
  }
    
  grid.cellCount += cardButtons.count
  grid.frame = gridRect
    
  if animated {
    dealCardsWithAnimation()
  }
}
I also had troubles with the device orientation changes while the cards were being dealt. In order to solve this issue, I had to cancel any running animations and rearrange the cards after the transition was done. In the controllers I had the following code:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  // In case the controller is still being presented and the
  // views haven't been instantiated.
  guard containerView != nil else { return }
    
  coordinator.animate(alongsideTransition: { _ in
    self.containerView.prepareForRotation()
  }) { _ in
    self.containerView.updateViewsFrames(withAnimation: true)
  }
}
 And in the container view:
/// Prepares the container for the device's rotation event.
/// Stops any running deal animations and respositions all the views.
func prepareForRotation() {
  animator.removeAllBehaviors()
    
  // Invalidates all scheduled deal animations.
  scheduledDealAnimations?.forEach { timer in
    if timer.isValid {
      timer.invalidate()
    }
  }
    
  positioningAnimator?.stopAnimation(true)
    
  for button in buttons {
    button.transform = .identity
    button.setNeedsDisplay()
  }
    
  isPerformingDealAnimation = false
}

Improvements

There’s some code that still needs refactoring. Also, none of the extra credit requirements regarding the Set game were executed, these are going to be my next steps.

The source code for this solution can be found in my Github repository.