11.
The App Structure
Written by Joey deVilla
In the previous chapter, you helped Checklist earn its name by giving it the capacity to store the “checked” status of checklist items and by giving the user the ability to check and uncheck items. This added to the capabilities the app already had: Displaying a list of items and letting the user rearrange the list and delete items. Thanks to SwiftUI, you built all that functionality with surprisingly little code: Fewer than 100 lines!
However, the app’s still missing some very important functionality. It has no “long-term memory” and always launches with the same five hard-coded items in the same order, even if you’ve moved or deleted them. There’s no way for the user to add new items or edit existing ones.
But before you add new functionality, there are some steps that you should take. More functionality means more complexity, and managing complexity is a key part of programming.
Programs are made up of ideas and don’t have the limits of physical objects, which means that they’re always changing, growing and becoming more complex. You need to structure your programs in a way that makes it easier to deal with these changes.
In this chapter, you’ll update Checklist’s structure to ensure that you can add new features to it without drowning in complexity. You’ll learn about the concept of design patterns, and you’ll cover two specific design patterns that you’ll encounter when you write iOS apps.
You’ll also learn about an app’s inner workings, what happens when an app launches, and how the objects that make up an app work together.
Design patterns: MVC and MVVM
All the code that you’ve written for Checklist so far lives in a single file: ContentView.swift. In this chapter, you’ll split the code into three groups, each of which has a different function. This will make your code easier to maintain in the future. Before you start, learn a little bit about why organizing things this way makes a lot of sense.
Different parts of the code do different things. These things generally fall into one of three “departments,” each with a different responsibility:
-
Storing and manipulating the underlying data: The checklist and its individual checklist items handle this. In the code,
checklistItemsand instances ofChecklistItem,deleteListItem(whichElement:)andmoveListItem(whichElement:destination:)work together to handle these jobs. -
Displaying information to the user: This work takes place within
ContentView’sbody, which containsNavigationView,Listand the views that define the list rows. Each of these includes each item’s name and checkbox. -
Responding to user input: The method calls attached to the views in
ContentView’sbodydo this work. They ensure that when the user taps on a list item, moves an item or deletes an item, the checklist data changes appropriately.
Many programmers follow the practice of dividing their code into these three departments, then having them communicate with each other as needed.
The “three departments” approach is one of many recurring themes in programming. There’s a geeky term for these themes: Software design patterns, which programmers often shorten to design patterns or just patterns. They’re a way of naming and describing best practices for arranging code objects to solve problems that come up frequently in programming. Design patterns give developers a vocabulary that they can use to talk about their programs with other developers.
Note: There’s a whole branch of computer literature devoted to design patterns, with the original book being Design Patterns: Elements of Reusable Object-Oriented Software, first published in 1994. Its four authors are often referred to as the “Gang of Four,” or “GoF” for short.
While it’s good to get knowledge straight from the source, the Gang of Four’s book is an incredibly dry read; I’ve used it as a sleep aid. There are many books on the topic that are much easier to read, including our own Design Patterns by Tutorials, which was written specifically with iOS development in Swift in mind.
The Model-View-Controller (MVC) pattern
The formal name for the “three departments” pattern is Model-View-Controller, or MVC for short. Each name represents a category of object:
- Models: These objects contain your data and any operations on that data. For example, if you’re writing a cookbook app, the model would consist of the recipes. In a game, it would be the design of the levels, the player score and the positions of the monsters.
Models can interact with each other, and the way in which they interact — the business rules or the domain logic — is also determined by the models. In a game, the player and opponent models determine how the players and their various opponents interact.
In Checklist, the checklists along with their to-do items and the “move” and “delete” operations form the data model. Models are the keepers of the knowledge in the system.
- Views: These are the visual part of the app: Text, images, buttons, lists and their rows and so on. In a game, the views form the visual representation of the game world, such as the monster animations or a frag counter.
A view can draw itself and respond to user input, but it shouldn’t handle any app logic. It should be “dumb” in the sense that it only knows how to show data to the user, without knowing anything about that data or making any decisions about what it’s displaying. Many different apps can all use views like Lists, because they’re not tied to a specific data model.
- Controllers: A controller is an object that connects your data model objects to the views. It listens to taps on the views, makes the data model objects do calculations in response, and updates the views to reflect the new state of your model.
Here’s how model, view and controller objects fit together:
The flow of activity in MVC is circular. The user taps a button or enters information in the view, then the view notifies the controller. The controller interprets the user interaction and then contacts the model for the information it needs to complete the request. The model provides the information to the controller, which relays it to the view, which shows it to the user.
The Model-View-Controller pattern has been around since the 1970s. Most desktop, web and mobile apps that you use are built on this pattern, or something similar to it. That’s because it does a good job of separating the various aspects of an app into more manageable chunks. This pattern makes life easier for both the solo developer, who has to manage a lot of code alone and developer teams, who have to work on the same app without getting in each other’s way.
You’ll use the Model-View-Controller pattern later in this book, when you build apps with the UIKit framework. SwiftUI uses a slightly different pattern, called MVVM.
Model-View-ViewModel (MVVM)
Over the years since its introduction, programmers have come up with modified versions of the Model-View-Controller pattern that better fit their needs. One of these is Model-View-ViewModel, which is often shortened to MVVM.
Like Model-View-Controller, its name suggests that it has three different object categories:
-
Models: MVVM models, like MVC models, contain the data for the app.
-
Views: As with MVC, MVVM views present data to the user and accept user input. Unlike MVC, there’s very little code in an MVVM view, since they focus on the user interface and nothing else.
-
ViewModels: “ViewModel” is a clumsy name, but since the brightest minds in computer science haven’t come up with a better one, we’re stuck with it. The ViewModel gives the view the data and functionality it needs from the model, and nothing more. You can think of the ViewModel as the model’s “customer service representative” and the view as the “customer.”
Here’s how model, view and ViewModel objects fit together:
The flow of activity in MVVM is often described as more linear than in MVC. In MVC, the Controller acts as a central hub and has to know about both the view and the model. In MVVM, the view knows only about the ViewModel, and the ViewModel knows only about the model.
This linear arrangement of one-way relationships makes it easier to maintain, modify and even swap out components. The flexibility that MVVM offers is one reason why it’s popular with Windows and web app developers, and it’s why Apple adopted this approach for the SwiftUI framework.
Using MVVM with Checklist
Here’s how you’ll split up Checklist’s code:
-
The existing
ContentViewalready acts as the app’s view. To adopt MVVM for Checklist, you’ll extract the code that makes up the ViewModel and the model and put each set of code into its own file. -
The ViewModel is an object that contains the properties and methods that the view needs to show data to the user and to respond to the user’s actions. You’ll extract the list of items and the methods that manipulate the list from
ContentViewand put them into their own ViewModel object, which will live in its own file. -
The model is an object representing individual checklist items. You’ll extract
ChecklistItemfromContentViewand put it into its own file.
The end result will be an app that appears the same to the user, but to programmers, it’ll be better organized and easier to maintain and to add features to.
Along the way, you’ll learn about breaking up a project into easier-to-manage pieces, dive deeper into how objects work and learn what happens “under the hood” when the user launches an app.
There’s a lot to do in this chapter, so go ahead and get started!
Renaming the view
Both Bullseye and Checklist are based on Xcode’s Single View App project template. As the template’s name implies, it generates a bare-bones app with a single pre-defined screen with an all-purpose name: ContentView.
It’s a good enough name for an app with a single screen, but a bit too generic for an app that will have multiple screens.
Since you’re in the process of rearranging the app to fit the Model-View-ViewModel pattern, give the app’s main view a more fitting name: ChecklistView.
You can do this manually, by searching for ContentView throughout the project’s files and renaming the ContentView.swift file itself, but that’s a tedious and error-prone process. Instead, you’ll rename it automatically using Xcode’s refactoring tools.
➤ Open ContentView.swift. Find the opening line of ContentView. Select ContentView and right-click or control-click it. Select Refactor from the menu that appears, then select Rename… from the next menu:
Xcode will scan all the project files for any occurrence of the name ContentView. It will then display all these occurrences in the Editor pane so you can easily rename them.
➤ Type ChecklistView. This change will be reflected in most of the instances of ContentView:
➤ Click Rename. All the instances of ContentView that matter — in the code and in filenames — will be changed to ChecklistView.
There’s a bug in Xcode’s refactoring that causes it to fail to update the one instance of ContentView that appears in the comments. It won’t affect the app, but you should change that instance manually, if only to be consistent.
➤ Build and run to reassure yourself that you didn’t break anything during the name change.
Adding a file for the model
Creating a file for the model
Now that you’ve given the app’s main view a better name, you’ll need to create files for the other objects in the MVVM pattern. You’ll start by creating a file for the model’s code.
➤ Add a new file to the project by right-clicking or control-clicking on the Checklist folder in Xcode’s Project Explorer. Select New File… from the menu that appears:
This time, you don’t want to add a new view to the app, but rather to a class. This calls for adding a different kind of file to the project.
➤ In the window that appears, make sure that you’ve selected iOS then select Swift File, and not SwiftUI View. Unlike the SwiftUI View template, which comes with code to generate a simple “Hello World!” screen, this option gives you a mostly empty file. Click Next:
➤ Enter ChecklistItem into the Save As: field. Make sure that you’ve selected ChecklistItem in the Group menu and in the Targets menu, then click Create:
The project now has a new file named ChecklistItem.swift.
Moving the model code to the file
Now that you have a new file for the model, it’s time to move its code there. Luckily, this is a simple process.
➤ Open ChecklistView.swift and cut the declaration of ChecklistItem:
struct ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked: Bool = false
}
➤ Then paste it into ChecklistItem.swift so that the code in the file looks like this:
import Foundation
struct ChecklistItem: Identifiable {
let id = UUID()
var name: String
var isChecked: Bool = false
}
➤ Build and run the app. It still works because you haven’t changed any code; you merely relocated the definition of ChecklistItem into its own file.
That takes care of the model code. It’s now time to tackle the ViewModel!
Adding a file for the ViewModel
Your next step is to create a file for the ViewModel.
➤ Add another new file to the project by right-clicking or control-clicking the Checklist folder in Xcode’s Project Explorer. Select New File… from the menu:
➤ Once again, make sure that you’ve selected iOS in the new window, then select Swift File and click Next:
➤ Enter Checklist into the Save As: field. Make sure that you’ve selected Checklist in the Group menu and in the Targets menu, then click Create:
The project now has a new file named Checklist.swift. You just need to add the ViewModel code to it.
Moving the ViewModel code to the file
Add the following to Checklist.swift, just after the import Foundation line:
class Checklist: ObservableObject {
}
A class is another kind of blueprint for objects. Like a struct, it has properties and methods and you can create class instances. In the spirit of “show, don’t tell,” continue with moving the ViewModel code into this class and see how it works in action before getting into a deeper discussion about classes.
You may not know much about classes yet, but you should have enough of a grasp of Swift syntax to know that you just added the definition of a class named Checklist and that it’s a kind of ObservableObject.
ObservableObject is a protocol, and as its name implies, when you use it on an object, another object can observe it for changes. That other object is called an observer.
In the MVVM pattern, the view observes the ViewModel. This observer/observed relationship binds the view and ViewModel so that when data that’s displayed to the user is updated in the ViewModel, the view updates itself automatically.
The ViewModel should contain all the functionality that the view needs to present information to the user. So far, that functionality is:
- Displaying the checklist items.
- Deleting an item.
- Moving an item.
- Toggling an item between “checked” and “unchecked”.
Start by moving the code for the first item on that list: Displaying the checklist items. The checklist items are stored in checklistItems in ChecklistView.
➤ Open ChecklistView.swift. Cut checklistItems in ChecklistView:
@State var checklistItems = [
ChecklistItem(name: "Walk the dog", isChecked: false),
ChecklistItem(name: "Brush my teeth", isChecked: false),
ChecklistItem(name: "Learn iOS development", isChecked: true),
ChecklistItem(name: "Soccer practice", isChecked: false),
ChecklistItem(name: "Eat ice cream", isChecked: true),
]
Xcode will show some error messages. Ignore them for now.
➤ Paste checklistItems into Checklist in Checklist.swift. Change @State to @Published.
The class should now look like this:
class Checklist: ObservableObject {
@Published var checklistItems = [
ChecklistItem(name: "Walk the dog", isChecked: false),
ChecklistItem(name: "Brush my teeth", isChecked: false),
ChecklistItem(name: "Learn iOS development", isChecked: true),
ChecklistItem(name: "Soccer practice", isChecked: false),
ChecklistItem(name: "Eat ice cream", isChecked: true),
]
}
Marking a property in an ObservableObject as @Published means that making changes to that property notifies any observing objects. By marking checklistItems as @Published, any changes to it — toggling an item, deleting an item or moving an item — will update any views that are observing the ViewModel.
That takes care of all the ViewModel properties. It’s time to move the ViewModel methods into Checklist.
➤ Open ChecklistView.swift. Cut the methods from ChecklistView:
func printChecklistContents() {
for item in checklistItems {
print(item)
}
print("===================")
}
func deleteListItem(whichElement: IndexSet) {
checklistItems.remove(atOffsets: whichElement)
printChecklistContents()
}
func moveListItem(whichElement: IndexSet, destination: Int) {
checklistItems.move(fromOffsets: whichElement, toOffset: destination)
printChecklistContents()
}
➤ Then go back to Checklist.swift and paste the methods into Checklist, just after the checklistItems.
Checklist should now look like this:
class Checklist: ObservableObject {
@Published var checklistItems = [
ChecklistItem(name: "Walk the dog", isChecked: false),
ChecklistItem(name: "Brush my teeth", isChecked: false),
ChecklistItem(name: "Learn iOS development", isChecked: true),
ChecklistItem(name: "Soccer practice", isChecked: false),
ChecklistItem(name: "Eat ice cream", isChecked: true),
]
func printChecklistContents() {
for item in checklistItems {
print(item)
}
print("===================")
}
func deleteListItem(whichElement: IndexSet) {
checklistItems.remove(atOffsets: whichElement)
printChecklistContents()
}
func moveListItem(whichElement: IndexSet, destination: Int) {
checklistItems.move(fromOffsets: whichElement, toOffset: destination)
printChecklistContents()
}
}
The ViewModel, Checklist, now has the following:
- A property called
checklistItems, which contains the checklist items. - Three methods:
-
printChecklistContents(): Prints the contents of
checklistItemsto Xcode’s debug console. - deleteListItem(whichElement:): Deletes a specified item from the checklist.
- moveListItem(whichElement:destination:): Moves a specified item to a new location in the list.
-
printChecklistContents(): Prints the contents of
These methods are all that the view currently needs to present information to the user and respond to the user’s actions – exactly what a ViewModel provides to a view.
At this point, you have a file for the model that includes its object blueprint, ChecklistItem.swift, ChecklistItem, a file for the ViewModel with its object blueprint, Checklist.swift and Checklist. You put these together from bits and pieces extracted from ChecklistView.swift and ChecklistView.
Now, take a look at what’s left of the view file and its object blueprint.
➤ Open ChecklistView.swift and look at ChecklistView. There’s a lot less code and a few more errors:
It appears that the view will need a little tweaking before Checklist’s new MVVM setup will work. The problem is that there isn’t a connection between the view, ChecklistView and the ViewModel, Checklist.
You’ll establish that connection in a while, after you learn a little more about objects.
Structs and classes
Until this chapter, the only kind of object blueprint you’ve worked with was a struct. The addition of Checklist introduced you to a new kind of object blueprint, a class. How are classes and structs the same, and how are they different?
Both are used to create instances or objects. Both have properties, which are what the objects know, and methods, which are what the objects do.
They also differ in a few ways, the most notable one being that structs are value types and classes are reference types. Rather than give you a dry technical definition of what these are, or confuse you with an analogy, your next step is to play with them using an Xcode feature called playgrounds.
Starting a new playground
A playground is a type of Xcode project that lets you experiment with Swift code and see the results immediately. Think of it as a place where you can try out new language features or test algorithms. Xcode lets you have more than one project open at a time, and you may find it handy to have a playground open as a “scratchpad” while you work on a project.
➤ In Xcode’s File menu, select New…, and then Playground.
You’ll see a pop-up where you select options for the playground you want to create:
I’ve found that the best kind of playground for playing with Swift language features is the blank macOS playground. That’s because it doesn’t load all the extra material that iOS and tvOS programming require, and it crashes less often. Here are the options to choose to create this kind of playground.
➤ In the pop-up, select macOS, highlight the Blank playground type, then click Next. You’ll see a Save As: dialog;
➤ Enter a name for the playground; I used Structs and classes. In the Add to: menu, select Don’t add to any project or workspace. Once you’ve done that, click the Create button.
Xcode will create a new playground, which will look like this:
You can see what all the code up to and including a particular line in the playground does by moving the cursor over its line number and pressing the “Play” button that appears. The results will appear in the live view column on the right.
➤ Move the cursor over the number for line 3 in the playground and click the “Play” button:
You should see “Hello, playground” appear in the live view column. That’s the value that was assigned to the variable str.
➤ Add the following line to the playground:
print("str contains: \(str)")
➤ Move the cursor over the line number of the line you just entered and click the “Play” button. You should see this:
print() works in playgrounds just like it does in iOS projects: It prints to the debug console.
Now that you’ve covered playgrounds and their basics, it’s time to use yours to learn about value types.
Value types
A “value type” is a type of data where each instance keeps its own copy. In Swift, numbers are value types. Play with a couple of numbers so you can see what this means.
➤ Add the following to the playground, after the code you entered previously:
var firstNumber = 5
var secondNumber = firstNumber
print("firstNumber contains \(firstNumber) and secondNumber contains \(secondNumber)")
➤ Move the cursor over the line number for the last line and click the “Play” button.
The debug console should show the text: “firstNumber contains 5 and secondNumber contains 5”. This makes sense; the line var secondNumber = firstNumber copies the contents of firstNumber into secondNumber.
➤ Add the following to the playground, after the code you entered previously:
secondNumber = 10
print("firstNumber contains \(firstNumber) and secondNumber contains \(secondNumber)")
➤ Move the cursor over the line number for the last line and click the “Play” button.
The debug console should show the text “firstNumber contains 5 and secondNumber contains 10”. Changing the value of secondNumber did not change the value of firstNumber. This is what “each instance keeps its own copy” means.
Structs are also value types. Next, you’ll define a simple struct and play with it, like you just did with numbers.
➤ Add the following to the playground, after the code you entered previously:
struct PetValueType {
var name: String = ""
var species: String = ""
}
Now, create an instance of PetValueType and a copy of that instance.
➤ Add the following to the playground, after the code you entered previously:
var pet1 = PetValueType()
pet1.name = "Fluffy"
pet1.species = "cat"
var pet2 = pet1
print("pet1: \(pet1.name) is a \(pet1.species)")
print("pet2: \(pet2.name) is a \(pet2.species)")
➤ Move the cursor over the line number for the last line and click the “Play” button.
The output in the debug console shows that both pet1 and pet2’s name properties are set to “Fluffy”, and their species properties are both set to “cat”.
➤ Add the following to the playground after the code you entered previously:
pet2.name = "Spot"
pet2.species = "dog"
print("pet1: \(pet1.name) is a \(pet1.species)")
print("pet2: \(pet2.name) is a \(pet2.species)")
➤ Move the cursor over the line number for the last line and click the “Play” button.
From the output in the debug console, you should see that pet1’s name and species properties are still “Fluffy” and “cat”, but pet2’s name and species properties are now “Spot” and “dog”. pet1 and pet2 are two separate values. Use value types for data where each instance is guaranteed to be its own thing and independent of any other instance. The individual checklist items in your app should be separate entities, which is why the ChecklistItem object blueprint is a value type — a struct.
Reference types
Classes are reference types. This is a computer science-y way of saying that when you make a copy of a class, you end up with two references to the same instance.
Once again, take a look at a code example. Define an object blueprint with the same properties as PetValueType, but as a class rather than a struct.
➤ Add the following to the playground, after the code you entered previously:
class PetReferenceType {
var name: String = ""
var species: String = ""
}
Now, create an instance of PetValueType and a copy of that instance.
➤ Add the following to the playground after the code you entered previously:
var pet3 = PetReferenceType()
pet3.name = "Tonkatsu"
pet3.species = "pot-bellied pig"
var pet4 = pet3
print("pet3: \(pet3.name) is a \(pet3.species)")
print("pet4: \(pet4.name) is a \(pet4.species)")
➤ Move the cursor over the line number for the last line and click the “Play” button.
In the debug console, you’ll see that both pet3 and pet4’s name properties are set to “Tonkatsu”, and their species properties are set to “pot-bellied pig”.
Now, check what happens when you change the values for pet4.
➤ Add the following to the playground, after the code you entered previously:
pet4.name = "Sashimi"
pet4.species = "goldfish"
print("pet3: \(pet3.name) is a \(pet3.species)")
print("pet4: \(pet4.name) is a \(pet4.species)")
Note the output in the debug console: “pet3: Sashimi is a goldfish” and ”pet4: Sashimi is a goldfish“. Both pet3 and pet4 are references to the same thing, which means that changing one changes the other.
Use reference types for data that different parts of an app share, or if the data needs the features that only a class offers.
The array of checklist items in your app is a shared resource that different screens will use. For this reason, the Checklist object blueprint is a reference type — a class.
It’s time to switch away from the playground and turn your attention to the last component of your app’s Model-View-ViewModel pattern: The view.
Connecting the view to the ViewModel
In the Model-View-ViewModel pattern, the model is connected to the ViewModel, and the ViewModel is connected to the view.
➤ To see the connection between the model and ViewModel, open Checklist.swift and look at the checklistItems array.
The connection between model and ViewModel is checklistItems, which is a property of Checklist, the ViewModel. Each element of checklistItems contains an instance of ChecklistItem, the model object.
I’ve mentioned it before, but we’ve been going over so much new material that it’s worth repeating: The key to connecting the ViewModel to the view is in the first line of Checklist:
class Checklist: ObservableObject {
And in the first line of the declaration of checklistItems:
@Published var checklistItems = [
Checklist adopts the ObservableObject protocol, which means that an observer can constantly watch its @Published properties and be notified if their values change. Now, you need to set up the view, ChecklistView, as an observer of Checklist.
➤ Open ChecklistView.swift. Here’s what the code for ChecklistView should look like:
struct ChecklistView: View {
// Properties
// ==========
// User interface content and layout
var body: some View {
NavigationView {
List {
ForEach(checklistItems) { checklistItem in
HStack {
Text(checklistItem.name)
Spacer()
Text(checklistItem.isChecked ? "✅" : "🔲")
}
.background(Color.white) // This makes the entire row clickable
.onTapGesture {
if let matchingIndex =
self.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
self.checklistItems[matchingIndex].isChecked.toggle()
}
self.printChecklistContents()
}
}
.onDelete(perform: deleteListItem)
.onMove(perform: moveListItem)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle("Checklist")
.onAppear() {
self.printChecklistContents()
}
}
}
// Methods
// =======
}
You’ve reduced ChecklistView to a single property, body, which describes the user interface. This is typical for views in SwiftUI — they contain only those things which define the user interface, and that’s done entirely with properties.
Back when the entire app lived in this file, the List view that displayed the checklist items got its data from checklistItems, an array that was both a @State and a property of the original ContentView. That array still exists; it’s a @Published property of Checklist, which is an ObservableObject.
You need the view to create an instance of Checklist and then observe it.
Add this line to ChecklistView, after the “Properties” comment and before the declaration for body:
@ObservedObject var checklist = Checklist()
This adds a new property to ChecklistView named checklist. The = sign means “put whatever is on the right side of me into checklist,” and Checklist() means “create a new instance of Checklist.” This is ChecklistView’s connection to Checklist — the view’s connection to the ViewModel. As an @ObservedObject, checklist will always keep the view up-to-date with any changes to its @Published properties.
Now that you’ve made the connection to Checklist, you just need to update body so that it refers to its required properties and methods in the ViewModel.
The array that body used to refer to, checklistItems, is now the checklistItems property of the checklist instance. Next, you’ll use Xcode’s “Find and Replace” feature to replace any occurrence of checklistItems in ChecklistView with checklist.checklistItems.
➤ In the Find menu, select Find and Replace…. You can also use the keyboard shortcut, Command+Option+F.
The “Find and Replace” function will appear at the top of the editor:
➤ Enter checklistItems into the Replace field and checklist.checklistItems into the With field, then click the All button.
There are also a couple of calls to printChecklistContents(), which was also moved to Checklist. Once again, “Find and Replace” will fix this.
➤ Enter printChecklistContents() into the Replace field and checklist.printChecklistContents() into the With field, then click the All button.
The last of the error messages will disappear, and the code for ChecklistView should look like this:
struct ChecklistView: View {
// Properties
// ==========
@ObservedObject var checklist = Checklist()
// User interface content and layout
var body: some View {
NavigationView {
List {
ForEach(checklist.checklistItems) { checklistItem in
HStack {
Text(checklistItem.name)
Spacer()
Text(checklistItem.isChecked ? "✅" : "🔲")
}
.background(Color.white) // This makes the entire row clickable
.onTapGesture {
if let matchingIndex =
self.checklist.checklistItems.firstIndex(where: { $0.id == checklistItem.id }) {
self.checklist.checklistItems[matchingIndex].isChecked.toggle()
}
self.checklist.printChecklistContents()
}
}
.onDelete(perform: checklist.deleteListItem)
.onMove(perform: checklist.moveListItem)
}
.navigationBarItems(trailing: EditButton())
.navigationBarTitle("Checklist")
.onAppear() {
self.checklist.printChecklistContents()
}
}
}
// Methods
// =======
}
➤ Build and run. It works as before, but it’ll be easier to maintain and upgrade now that it’s been neatly divided into model, ViewModel, and view components.
Refactoring once more
You still have one more change to the code to make…
checklistItems’s name comes from the time when it was a property of the old ContentView. Now that it’s a property of Checklist, the code in ChecklistView accesses it using the unnecessarily wordy checklist.checklistItems.
Let’s change Checklist’s checklistItems property’s name to items.
➤ In Checklist.swift, select checklistItems, right-click or control-click on it, select Refactor ➤ and then Rename…:
➤ Type items and click the Rename button to rename the property across all the code in the project:
➤ Build and run to confirm that this change didn’t break it.
What happens when you launch an app?
In its new Model-View-ViewModel configuration, here’s how each of the objects that make up the app is created:
When the app launches, the view object, checklistView, is created first. The view has two properties: body, which defines the user interface, and checklist, which is the connection to the ViewModel.
The Checklist() part of this line in ChecklistView:
@ObservedObject var checklist = Checklist()
creates an instance of Checklist, which brings the app’s ViewModel into existence.
In Checklist, the declaration of the array you renamed to items:
@Published var items = [
ChecklistItem(name: "Walk the dog", isChecked: false),
ChecklistItem(name: "Brush my teeth", isChecked: false),
ChecklistItem(name: "Learn iOS development", isChecked: true),
ChecklistItem(name: "Soccer practice", isChecked: false),
ChecklistItem(name: "Eat ice cream", isChecked: true),
]
Creates an instance of ChecklistItem for each instance of an item in the list.
Simply put, an instance of ChecklistView creates an instance of Checklist, which in turn creates a number of instances of ChecklistItem.
But what starts the process? What creates the instance of ChecklistView?
The app delegate and scene delegate
As you learned back in Chapter 2, “The One-Button App,” an Xcode project includes a number of source files that contain code to support the app you’re writing. This code handles all the behind-the-scenes details necessary to make a mobile app work, freeing you to focus on the code that’s specific to your app.
You may have noticed two of these files in both the Bullseye and Checklist projects: AppDelegate.swift and SceneDelegate.swift. They appear in every iOS app that uses the SwiftUI framework.
Note: When you switch to building apps with the UIKit framework later in this book, you’ll only see AppDelegate.swift. UIKit predates SwiftUI, and all its startup code lives in AppDelegate.swift. With SwiftUI, the Apple developers decided to separate that code into two separate files, where AppDelegate.swift contains code for managing the overall app and SceneDelegate.swift is the place for code that manages the app’s individual windows, or scenes.
Every program, regardless of programming language and platform, has an entry point. It’s the start of the program — the first set of instructions that execute when the program launches. For iOS apps, the entry point is in app delegate whose code is in AppDelegate.swift.
You can think of the app delegate as your app’s “root object.” It manages your app at the system level, which includes initializing your app’s user interface.
➤ Open AppDelegate.swift. The code inside may look incomprehensible to you right now, but you should take note of a few things. The first one is this line:
class AppDelegate: UIResponder, UIApplicationDelegate {
You should interpret this line as “this is a class named AppDelegate, and it’s a kind of UIResponder and UIApplicationDelegate”.
In case you’re wondering, a UIApplicationDelegate defines what an app delegate does and a UIResponder is an object that responds to user interface events such as the user tapping the screen or wiggling their phone.
Here’s AppDelegate’s first method:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
This method’s name is application(_:didFinishLaunchingWithOptions:). It performs some tasks when the app has finished launching. One of those tasks is to put your app’s user interface on the screen, which it does by creating a scene delegate whose code is defined in SceneDelegate.swift.
On Apple platforms, a scene is an instance of an app’s user interface. On a macOS desktop app, which can have multiple windows, each window is contained within a scene. iOS apps have only one window, and therefore have only one scene.
Each scene has a scene delegate, which manages what happens to the scene under different circumstances. Take a look at those circumstances now.
➤ Open SceneDelegate.swift. As with AppDelegate.swift, the details of the code might not be clear to you, but you should note a few things.
First, it defines a class named SceneDelegate, which is a kind of UIResponder and UIWindowSceneDelegate. UIWindowSceneDelegate defines what an app delegate does and, like the app delegate, also responds to user interface events.
It also has a number of methods, whose names mostly begin with different circumstances that could arise while an app is running. These include sceneWillEnterForeground and sceneDidEnterBackground. Most of these methods contain nothing but comments; they’re there for advanced programmers to add code for custom behaviors to handle different circumstances.
However, the first method in the class does contain code:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
Remember: A scene is a container for a window in your app. In iOS apps, there’s only one window and it takes up the entire screen. This method determines which view is the first screen your app shows, and it does so with this code:
// Create the SwiftUI view that provides the window contents.
let contentView = ChecklistView()
Now, look at this code in detail. The first thing it does is declare a constant called contentView. As a constant, you can fill it with a value only once. Once filled, you can’t change it to another value as long as the app is running.
The = specifies that you’re going to fill contentView with a value, and that value is the thing that follows. In this case, it’s ChecklistView().
As a reminder, a capitalized name followed by parentheses, (), typically means that you’re creating an object or instance. That’s what’s happening with ChecklistView(): It tells Swift to create a new instance of ChecklistView , which is defined inside the ChecklistView.swift file. This new instance is stored inside contentView.
Note: The convention is that names for constants, variables, methods and functions start with lowercase letters, and names for data types and object blueprints like structs and classes start with uppercase letters.
Now that contentView has been defined as an instance of a ChecklistView screen, the next bit of code puts the contents of contentView into the scene’s window:
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
Here’s the line in the code above that matters:
window.rootViewController = UIHostingController(rootView: contentView)
This line makes contentView the first screen that the app displays. Since contentView contains an instance of ChecklistView, that first screen is your list of to-do items!
Now that you know about the app delegate and the scene delegate, here’s a more complete view of the objects in the app and how they’re created:
Changing the app’s first screen
Your next step is to change the app so that it starts with a screen other than ContentView. The app will need a couple of additional screens anyway, so add a screen that you’ll eventually use to edit items in the list.
➤ Add a new file by right-clicking or control-clicking on the Checklist folder in Xcode’s Project Explorer. Select New File… from the menu that appears:
➤ You want to add a new view to the app. So in the window that appears, make sure you’ve selected iOS, then select SwiftUI View and click Next:
➤ Enter the name of the new view, EditChecklistItemView, into the Save As: field. Make sure that you’ve selected Checklist in the Group menu and in the Targets menu, then click Create:
The project now has a new file, EditChecklistItemView.swift. If you open it, you’ll see the code for a new view, EditChecklistItemView. Here’s the interesting part of the file:
struct EditChecklistItemView: View {
var body: some View {
Text("Hello World!")
}
}
You’ve seen this before — this is the default content for a new view created by Xcode, which is an empty screen with the text “Hello World!” in the center. Your next step is to change SceneDelegate so that the app opens with it, instead.
➤ Open SceneDelegate.swift and find this line in scene(_:willConnectTo:options:):
let contentView = ChecklistView()
➤ Change the line to the following:
let contentView = EditChecklistItemView()
➤ Build and run. Instead of seeing the checklist, you’ll see the “Hello World!” screen from EditChecklistItemView:
You actually want ChecklistView to be the app’s first screen, so change the code back to what it was.
➤ Find this line:
let contentView = EditChecklistItemView()
➤ And change it back to this:
let contentView = ChecklistView()
➤ Build and run to confirm that ChecklistView is the screen that the app shows when it launches.
Congratulations! You’ve learned a lot about how your app works, and made some improvements to it along the way, including how best to utilise design patterns to better structure your code.
Key points
In this chapter, you did the following:
- You learned about design patterns in general, and specifically about two key patterns that you’ll use as an iOS developer: Model-View-Controller (MVC) and Model-View-ViewModel (MVVM).
- You renamed the app’s view from the default
ContentViewto one that better fits this project:ChecklistView. - You changed the architecture of the app so that all the code no longer lives in a single file, but in a model file, a ViewModel file and a view file.
- You learned about the app and scene delegate objects and about what happens when an app launches.
You’ll find the project files for the app at this stage under 11 - App Structure in the Source Code folder.
In the next chapter, you’ll add the next major feature: Adding and editing checklist items.