Object-Oriented Programming: Beyond the Basics

Oct 17 2023 · Swift 5.9, iOS 17, Xcode 15

Lesson 03: Design Patterns

Demo 3

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll implement the Observer pattern to allow the contacts book to receive update notifications whenever a contact is updated. Ideally, in a system with a user interface, the contacts book will also notify the UI to update, so it will reflect changes to individual contacts.

To start, create two new Swift files under the Sources folder. Name the first: Subject and the second: Observer.swift. Now, open subject.swift and add the following:

public protocol Subject: AnyObject {
}

Next, open Observer.swift and add:

public protocol Observer: AnyObject {
}

Both of those protocols have constraints that need to be implemented on classes and not structs, mainly because they rely on references. Value types get copied when you assign them to different properties or add them to an array. Subjects need references to the observers, not copies of them.

Start with Observer because it’s simpler:

public protocol Observer: AnyObject {
  func subjectUpdated(subject: any Subject)
}

subjectUpdated(:) is the method that each observer needs to implement to be notified when a subject receives an update. The affected subject is passed as a parameter. Without it, the observer won’t know which entry was updated.

Next, open Subject.swift. Add the protocol specifications:

public protocol Subject: AnyObject {
  var observers: [Observer] { get set }
  func addObserver(_ obj: Observer) 
  func removeObserver(_ obj: Observer) 
  func broadcastUpdates() 
}

The protocol consists of four parts. First, it takes in an array of observers. Next, it has a method adds an observer. Then it defines a method that removes the observer. Finally, it defines a method sends a notification to all the registered observers on the list that the subject has been updated.

Since Swift supports having implementations directly in protocols, you can implement those functions to save yourself a lot of trouble whenever you want to conform to Subject in your code.

To do so, add this code after the closing bracket of Subject (not inside it):

public extension Subject {
  func addObserver(_ obj: Observer) {
    observers.append(obj)
  }

  func removeObserver(_ obj: Observer) {
    observers.removeAll { item in
      item === obj
    }
  }

  func broadcastUpdates() {
    observers.forEach { observer in
      observer.subjectUpdated(subject: self)
    }
  }
}

Now, whenever you conform to Subject, you’ll receive those implementations directly. However, you’ll still need to define the observers array in your conforming type.

Go to ContactCard.swift. Change its declaration to conform to Subject and add an array of observers:

public class ContactCard: Subject {
  public var observers: [Observer]
  ...

Remember to initialize the array in the main constructor.

public init(firstName: String, lastName: String, phoneNumber: String) {
  self.firstName = firstName
  self.lastName = lastName
  self.phoneNumber = phoneNumber
  isCompany = false
  contactID = UUID()
  relatedContacts = []
  observers = [] // new code
}

Next, go to ContactsBook.swift and make it conform to Observer in an extension by adding the following at the end of the file, after the final closing bracket:

extension ContactsBook: Observer {
  public func subjectUpdated(subject: Subject) {
    let index = contactsList.firstIndex { contact in
      contact === subject
    }

    guard let index else {
      return
    }
    print("Contact at index \(index) has been updated")
  }
}

Here, you made ContactBook conform to the protocol and implemented subjectUpdated(:). The new function searches for the index of the provided subject (which is an instance of ContactCard) in the array of contacts. When it finds the index of the item, it prints out the index.

Go to your playground and apply any updates on the contacts you have:

ehabContact.set(phone: "333333333333")
ehabContact.set(phone: 44444444444)
timContact.set(firstName: "Nick", lastName: "Fury")

Before we run, comment out the current prints methods so we can have a clean console log.

//print(ehabContact.contactInformation())
//print(kodeco.contactInformation())
//print(razeware.contactInformation())

//book1.printContacts()

Open up ContactsBook.swift and do the same

//print("Contacts Book has \(contactsList.count) entries")

Make sure to leave the print statement you added in the extension.

Open up PersonContactCard.swfit and do the same.

// print("Calling super from Person")

//print("Other contact is a Person too. Add 2-way relationship")

Finally, do the same for CompanyContactCard.swft:

//print("Calling super from Company")

//print("Other contact is a company too. Adding 2-way relationship")

Run the program. Nothing is printed out to the console.

Well, that’s expected. Your observer didn’t subscribe to receive updates on any subject, so when you updated the subjects, they didn’t know who to inform. The array observers is empty. You’ll fix this next.

Open ContactsBook.swift. Add the following:

public func saveContact(contact: ContactCard) {
  contactsList.append(contact)
  contact.addObserver(self) // new code
}

Now, whenever ContactsBook receives a new contact to save, it subscribes to the new contact so that it can receive updates on it.

Run the program again. Still, nothing prints!

Well… you updated some contact cards, but they didn’t announce anything to the observers.

In ContactCard.swift, update all the methods that change the values or relationship to call broadcastUpdates() when they’re done:

public func addRelatedContact(_ contact: ContactCard) {
  relatedContacts.append(contact.contactID)
  broadcastUpdates() // new code
}

public func set(firstName: String, lastName: String) {
  self.firstName = firstName
  self.lastName = lastName
  broadcastUpdates() // new code
}

public func set(phone: String) {
  phoneNumber = phone
  broadcastUpdates() // new code
}

public func set(phone: Double) {
  phoneNumber = "\(phone)"
  broadcastUpdates() // new code
}

Run the program. This time, you’ll see print statements with the index of the contact in the book with each change. Congratulations!

See forum comments
Cinema mode Download course materials from Github
Previous: Introduction 3 Next: Conclusion