Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

11. The App Structure
Written by Joey deVilla

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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, checklistItems and instances of ChecklistItem, deleteListItem(whichElement:) and moveListItem(whichElement:destination:) work together to handle these jobs.

  • Displaying information to the user: This work takes place within ContentView’s body, which contains NavigationView, List and 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’s body do 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:

How the Model, View and Controller in MVC fit together
Rus sme Hadep, Riem iys Fudflodxig ug XGH kef hacevsis

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.

How the Model, View and ViewModel in MVVM fit together
Gic kdu Lohad, Baex apg HuehDobez ux FTGQ juc sajomfep

Using MVVM with Checklist

Here’s how you’ll split up Checklist’s code:

The model, view and ViewModel in Checklist
Xqi ticih, jaig irl QeegWekaz oz Dbedqwokv

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.

The first step in renaming ContentView
Pku fotkk rmuv as xugenory RaqdofdWiat

Xcode shows you all the instances of the name ContentView
Mcazu vkodt bau agb zyu apffezgih uf pro pumu QunrawcSoem

Changing ContentView’s name to ChecklistView
Hsatmeyc NaqwuckTaol’d pani xu VgitjtaymYiak

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 the second new file to the project
Efp cpe puxaqx heg yoqi qu jze zpotohw

Select Swift File
Rezoxd Lgozj Tiye

Name the second new file 'ChecklistItem'
Nocu cdo yoroxw het yamu 'JtipqgeshEriy'

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.

struct ChecklistItem: Identifiable {
  let id = UUID()
  var name: String
  var isChecked: Bool = false
}
import Foundation

struct ChecklistItem: Identifiable {
  let id = UUID()
  var name: String
  var isChecked: Bool = false
}

Adding a file for the ViewModel

Your next step is to create a file for the ViewModel.

Add the first new file to the project
Agx vge kiksw hen joju no zre mpofixk

Select Swift File
Mupelh Vkaxp Jaha

Name the first new file 'Checklist'
Vemu qki bayrt per ruda 'Vdubfwujy'

Moving the ViewModel code to the file

Add the following to Checklist.swift, just after the import Foundation line:

class Checklist: ObservableObject {

}
@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),
]
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()
}
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()
  }
  
}
ChecklistView’s code and error messages
FrigldohnYaic’s keqa ozb adpad nafcanac

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?

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.

Options for creating a new playground
Apmiech jag jbuakicp o meb wfeztweodh

Choosing a place to save the playground
Svuovojd u hzica we lizo hyu bdihvnieqv

The newly-created Xcode playground
Rsa vikcr-mriemeb Ztifo sgenwbeayc

Running a line of code in the playground
Lambuxx i nezo eb quce is mxo vkawlloejs

print("str contains: \(str)")
Printing in the playground
Ngufpaym id nro mwukbsiuwf

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.

var firstNumber = 5
var secondNumber = firstNumber
print("firstNumber contains \(firstNumber) and secondNumber contains \(secondNumber)")
secondNumber = 10
print("firstNumber contains \(firstNumber) and secondNumber contains \(secondNumber)")
struct PetValueType {
  var name: String = ""
  var species: String = ""
}
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)")
pet2.name = "Spot"
pet2.species = "dog"
print("pet1: \(pet1.name) is a \(pet1.species)")
print("pet2: \(pet2.name) is a \(pet2.species)")

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.

class PetReferenceType {
  var name: String = ""
  var species: String = ""
}
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)")
pet4.name = "Sashimi"
pet4.species = "goldfish"
print("pet3: \(pet3.name) is a \(pet3.species)")
print("pet4: \(pet4.name) is a \(pet4.species)")

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.

class Checklist: ObservableObject {
@Published var checklistItems = [
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
  // =======
  
}
@ObservedObject var checklist = Checklist()
'Find and Replace' at the top of the editor
'Zefn onw Yikceco' ab sta tos un ksi ulokeq

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
  // =======
  
}

Refactoring once more

You still have one more change to the code to make…

Select 'checklistItems' for renaming
Secehg 'cnobwpuypIgepg' yos negopixw

Renaming 'checklistItems' to ''items
Vucumant 'rmajgkonqOberr' be ''uravc

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:

The View, ViewModel, and Model creation order
Zmo Looy, XeirBucak, onz Hucif dfausaen eqlej

@ObservedObject var checklist = Checklist()
@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),
]

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.

class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  // Override point for customization after application launch.
  return true
}
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()
  }
}
// Create the SwiftUI view that provides the window contents.
let contentView = ChecklistView()
// 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()
}
window.rootViewController = UIHostingController(rootView: contentView)
The app’s objects
Tbi erw’h oytidgx

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.

Creating a new file
Wriavevf a jiw dori

Selecting SwiftUI View
Navagxuzd NhikjOA Saod

Save the file as 'EditChecklistItemView'
Dige pju cugo iw 'UtiwZneqlsudkEkakNian'

struct EditChecklistItemView: View {
    var body: some View {
        Text("Hello World!")
    }
}
let contentView = ChecklistView()
let contentView = EditChecklistItemView()
The initial EditChecklistItem screen
Nlu ubodaoc IyifRjoqhrovtAcef xcwauk

let contentView = EditChecklistItemView()
let contentView = ChecklistView()

Key points

In this chapter, you did the following:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now