Due to its declarative and reactive nature, SwiftUI works pretty well with this technique. Suppose we are building a screen that fetches a Github profile. We begin by describing the possible stages of this fetch process:
enum State: Equatable {
case `default`
case loading
case fetched(profile: GithubProfileViewModel)
case failure(error: HttpError)
}
In the view model, we declare a variable holding the state:
@MainActor
final class GithubProfileFetchViewModel: ObservableObject {
@Published
private(set) var state = State.default
func fetchProfile(using username: String) async {
// This method will update the state in the different stages
// of the fetch process. Notice the state is published.
}
// ...
}
Then in the View, we use this state when declaring our body:
struct GithubProfileFetchView: View {
@StateObject
private var viewModel = GithubProfileFetchViewModel()
// ...
var body: some View {
VStack {
// ...
switch viewModel.state {
case .`default`:
DefaultProfileView()
case .fetched(let profileViewModel):
ScrollView {
GithubProfileView(viewModel: profileViewModel)
.padding()
}
case .loading:
LoadingIndicator()
case .failure(let error):
if error == .requestFailed(statusCode: 404) {
ErrorView.profileNotFound()
} else {
ErrorView.connectionError()
}
}
// ...
}
.task(id: shouldStartFetch) {
await fetchProfile()
}
}
// ...
}
The usage of switch makes the code really readable. Whenever the state changes, SwiftUI presents a different view for us. Since SwiftUI is declarative, we don’t need to setup bindings, and manually change what we display. This process is all done automatically for us.
Note that this technique works with other architectures too, and with other property wrappers (e.g @State, @ObservableObject, @EnvironmentObject). You can check the full view model or view code.
You might also want to read this article by Apple.
The answer depends on how we use SwiftUI. For an app entirely written using it, one might argue that it gets released whenever the app finishes. But what about an UIKit app that uses some SwiftUI views?
To answer this question, let’s explore some scenarios involving environment objects. They have reference semantics, and we can track their instances using the memory graph.
If you wish to check the sample project, here’s the repository. It has different git tags for each scenario explored below.
Scenario 1: holding a View instance
Let’s begin our exploration by instantiating our RootView with some environment objects. We won’t attach it to the UI, but only store it in a variable in our view controller:
SwiftUI needs a way to bind these environment objects to a specific view. They will stay around as long as we hold the RootView instance. If we build the memory graph, here’s what it shows:
Scenario 1: holding a RootView instance.
Notice the memory graph doesn’t show instances of SwiftUI views. These are structs, and have value semantics, not living in the heap.
Scenario 2: Holding a reference to a UIHostingController instance
This is similar to the first scenario. If we hold a reference to a UIHostingController, the environment will still be around, as the controller holds the value of its root view. Notice this happens even after the SwiftUI views get removed from the view hierarchy:
@objc private func releaseRootView() {
guard let rootViewHostingController,
let hostingView = rootViewHostingController.view else {
return
}
rootViewHostingController.removeFromParent()
hostingView.removeFromSuperview()
// Scenario 2: Keep holding a reference to the hosting controller.
//self.rootViewHostingController = nil
}
The memory graph continues showing our two instances in memory:
Scenario 2: Referencing a UIHostingController
Scenario 3: Having a UIView with a retain cycle
In this scenario, we explore UIViews used within a SwiftUI view tree. UIViews have reference semantics, and if an instance leaks in memory, the SwiftUI environment will continue alive:
final class LeakingUIView: UIView {
private var retainingClosure: (() -> ())!
override func layoutSubviews() {
super.layoutSubviews()
retainingClosure = {
// Scenario 3: Having a retain cycle in a UIView.
self.backgroundColor = .red
}
retainingClosure()
}
}
struct LeakingView: UIViewRepresentable {
func makeUIView(context: Context) -> LeakingUIView {
LeakingUIView()
}
func updateUIView(_ uiView: LeakingUIView, context: Context) {}
}
When we release our hosting controller instance, we notice the environment is still being held due to references associated with the leaking view in memory.
Here’s what the memory graph shows:
LeakingView has a reference a UITraitCollection
UITraitCollection has a reference to a SwiftUIEnvironmentWrapper
SwiftUIEnvironmentWrapper has references to its environment objects (EnvA, EnvB)
Scenario 4: Having a retain cycle between a UIView and an environment object
If we hold a reference to a UIView in one of the environment objects, and this UIView was inside a SwiftUI tree, we have a reference cycle:
After removing the SwiftUI views from the screen, here’s what the memory graph shows:
Scenario 4: a retain cycle between SubUIView and EnvA
SubUIView has a reference to a UITraitCollection
UITraitCollection has a reference to a SwiftUIEnvironmentWrapper
SwiftUIEnvironmentWrapper has a reference to EnvA
EnvA has a reference to SubUIView, and we can go back to the point 1 (a cycle)
Conclusion
Always make sure to:
Dispose of any SwiftUI View values not used anymore
Dispose of any UIHostingController references not used anymore
Watch out for memory leaks in:
UIViews used within SwiftUI
references between your UIViews and your environment objects
UIViewControllers presenting the UIHostingControllers
the environment objects themselves
Environment objects can get complex depending on what your views need to accomplish. They might use a lot of computational resources, so it’s necessary to watch out for live instances when the underlying views get deallocated.
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:
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:
Use swift build -c release to build the executable using the release configuration
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:
echo $PATH, this will display the folders separated by colon characters (:)
Rename the executable to an invokable name:
mv .build/release/Simple\ Executable ./say-hello
If /usr/local/bin doesn’t exist, create it with this command: sudo mkdir /usr/local/bin
sudo mv ./say-hello /usr/local/bin
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.
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:
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:
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.
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.
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.
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):
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:
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:
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.
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:
In the navigation area, select the project
At the left bar, select the project again (not the target)
At the top tab bar, select the Package Dependencies tab
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:
Look at the URL and version of each package
Fetch each dependency with the correct version
Repeat this process for each sub dependency of a package if it has any
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:
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:
You understand what the package does and what sub dependencies it uses
It comes from a trusted source
If it’s an open-source package, ensure it’s maintained by the community (check the latest changes, number of stars, and so on)
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.
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.
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.
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:
Create an app group and share it with the app and the extension.
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.
Use the new location in my DataController (the object holding the whole CoreData stack).
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!)
}
}
}
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.