Observer Pattern Part #1

The Observer Pattern is a behavioral design pattern if you don't know what design pattern see this POST. Understanding this pattern is fundamental because it is common in the iOS developer day-to-day.

What is Observer Pattern

It makes a one-to-many communication. According to the definition, this pattern uses an object when updated, all its dependents will be updated. Unlike the delegate pattern, which uses one-to-one communication.

How to create:

The observer pattern can be seen in several ways we have:

  • KVO
  • Manual
  • Notification center
  • RXswift
  • Combine

In this post, we will talk about the manual form. The blog has a post about KVO and notification Center (here), and RxSwift and combine is another field, which uses reactive programming.

Application:

This site reproduces a very good example of how to apply the observer pattern, also it explains in more detail what it is. Link

What we will illustrate in this post is an example of an auction. When the value is changed, all the plates of the auction are changed.

Let's go to Practice:

The result is this:

Kapture 2021-03-20 at 18 59 11

The structure of the project will look like this:

Screen Shot 2021-03-20 at 15 44 15

Note: The UI component that we used in the last post, had some changes. So it will be explained only the changes.

ViewController.swift

import UIKit

class ViewController: UIViewController {
    let uiAuction = UIAuction()

    let viewModel = AuctioneerViewModel()

    override func loadView() {

        self.view = uiAuction

    }

    override func viewDidLoad() {
        super.viewDidLoad()

    }

}

UIAuction.swift


import UIKit

class UIAuction: UIView {


    let personOne: Person = {
        let person = Person()
        person.btnSendValue.accessibilityIdentifier = "person1"
        person.tfValueFirstPerson.accessibilityIdentifier = "person1"
        person.lbName.text = "person 1"

        return person
    }()

    let personTwo: Person = {
        let person = Person()
        person.btnSendValue.accessibilityIdentifier = "person2"
        person.tfValueFirstPerson.accessibilityIdentifier = "person2"
        person.lbName.text = "person 2"

        return person
    }()

    let personThree: Person = {
        let person = Person()
        person.btnSendValue.accessibilityIdentifier = "person3"
        person.tfValueFirstPerson.accessibilityIdentifier = "person3"
        person.lbName.text = "person 3"

        return person
    }()

    let auctioneer = Auctioneer()

    let lbwinner: UILabel = {
        let lb = UILabel()
        lb.backgroundColor = .lightGray
        lb.font = .systemFont(ofSize: 25, weight: .bold)
        lb.textAlignment = .center
        lb.text = "Winner"
        lb.translatesAutoresizingMaskIntoConstraints = false
        return lb
    }()


    var container: UIStackView = {
        let container = UIStackView(frame: .zero)
        container.axis = .horizontal
        container.distribution = .fillEqually
        container.spacing = 5

        container.translatesAutoresizingMaskIntoConstraints = false

        return container
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.backgroundColor = .systemBackground

        self.addUIAuctioneer()
        self.addUIPerson()

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }



    func addUIAuctioneer(){
        self.addSubview(auctioneer)

        NSLayoutConstraint.activate([

            self.auctioneer.topAnchor.constraint(equalTo: self.layoutMarginsGuide.topAnchor),
            self.auctioneer.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            self.auctioneer.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 0.3),

        ])
    }

    func addUIPerson(){

        container.addArrangedSubview(personOne)
        container.addArrangedSubview(personTwo)
        container.addArrangedSubview(personThree)

        self.addSubview(container)
        self.addSubview(lbwinner)

        NSLayoutConstraint.activate([

            self.container.centerYAnchor.constraint(equalTo:self.layoutMarginsGuide.centerYAnchor),
            self.container.centerXAnchor.constraint(equalTo:self.layoutMarginsGuide.centerXAnchor),

            self.container.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            self.container.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            self.container.heightAnchor.constraint(equalToConstant: 200),

            self.lbwinner.topAnchor.constraint(equalTo: self.container.bottomAnchor, constant: 10),
            self.lbwinner.trailingAnchor.constraint(equalTo: self.trailingAnchor),
            self.lbwinner.leadingAnchor.constraint(equalTo: self.leadingAnchor),

        ])
    }

}

As can be seen, I added a label to show the winner of the auction and named our auction participants.

   let lbwinner: UILabel = {
        let lb = UILabel()
        lb.backgroundColor = .lightGray
        lb.font = .systemFont(ofSize: 25, weight: .bold)
        lb.textAlignment = .center
        lb.text = "Winner"
        lb.translatesAutoresizingMaskIntoConstraints = false
        return lb
    }()

Note: don't forget to add the visualization and do the autoLayout, as included in the code.

Auctioneer.swift

import UIKit


class Auctioneer: UIView {


    //Atributtes
    let imPerson: UIImageView = {
        let iv = UIImageView()
        iv.image = UIImage.init(systemName: "person")
        iv.contentMode = .scaleAspectFit
        iv.translatesAutoresizingMaskIntoConstraints = false

        return iv
    }()

    var lbValue: UILabel = {
        let lb = UILabel()
        lb.backgroundColor = .gray
        lb.font = .systemFont(ofSize: 25, weight: .bold)
        lb.textAlignment = .center
        lb.text = "Value"
        lb.translatesAutoresizingMaskIntoConstraints = false

        return lb
    }()

    let lbTime: UILabel = {
        let lb = UILabel()
        lb.backgroundColor = .gray
        lb.font = .systemFont(ofSize: 25, weight: .bold)
        lb.textAlignment = .center
        lb.text = "7"
        lb.translatesAutoresizingMaskIntoConstraints = false

        return lb
    }()

    let container: UIStackView = {
        let stack = UIStackView(frame: .zero)
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.spacing = 8
        stack.translatesAutoresizingMaskIntoConstraints = false

        return stack
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.translatesAutoresizingMaskIntoConstraints = false

        addAttributes()
        addConstraints()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func addAttributes(){
        self.container.addArrangedSubview(imPerson)
        self.container.addArrangedSubview(lbTime)
        self.container.addArrangedSubview(lbValue)

        self.addSubview(container)
    }


    func addConstraints(){

        NSLayoutConstraint.activate([

            self.container.bottomAnchor.constraint(equalTo: self.bottomAnchor),

            self.container.topAnchor.constraint(equalTo: self.topAnchor),
            self.container.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            self.container.trailingAnchor.constraint(equalTo: self.trailingAnchor),

        ])
    }

}

Person.swift

Person class, we will have to make some changes:

We will create a label to indicate the name of the participants, add and add to the container.

//Atributtes

var lbName: UILabel = {
        let lb = UILabel()
        lb.font = .systemFont(ofSize: 15, weight: .bold)
        lb.textAlignment = .center
        lb.text = "Name"
        lb.translatesAutoresizingMaskIntoConstraints = false

        return lb
    }()

    let imPerson: UIImageView = {
        let iv = UIImageView()

        iv.image = UIImage.init(systemName: "person")
        iv.contentMode = .scaleAspectFit
        iv.translatesAutoresizingMaskIntoConstraints = false

        return iv
    }()

    let tfValueFirstPerson: UITextField = {
        let tf = UITextField()
        tf.placeholder = "Enter value"
        tf.font = UIFont.systemFont(ofSize: 25)
        tf.borderStyle = UITextField.BorderStyle.roundedRect
        tf.keyboardType = UIKeyboardType.numberPad
        tf.returnKeyType = UIReturnKeyType.done
        tf.clearButtonMode = UITextField.ViewMode.whileEditing

        tf.translatesAutoresizingMaskIntoConstraints = false

        return tf
    }()

    var btnSendValue: UIButton = {
        let btn = UIButton()
        btn.setTitle("Send", for: .normal)
        btn.backgroundColor = .green
        btn.titleLabel?.font = .systemFont(ofSize: 25, weight: .medium)
        btn.translatesAutoresizingMaskIntoConstraints = false
        btn.addTarget(self, action: #selector(setValueFirstPerson), for: .touchUpInside)

        return btn
    }()

    let lbValue: UILabel = {
        let lb = UILabel()
        lb.backgroundColor = .gray
        lb.font = .systemFont(ofSize: 25, weight: .bold)
        lb.textAlignment = .center
        lb.text = "Value"
        lb.translatesAutoresizingMaskIntoConstraints = false

        return lb
    }()


    let container: UIStackView = {
        let stack = UIStackView(frame: .zero)
        stack.axis = .vertical
        stack.distribution = .fillEqually
        stack.spacing = 8

        stack.translatesAutoresizingMaskIntoConstraints = false

        return stack
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.translatesAutoresizingMaskIntoConstraints = false

        addPerson()
        addConstraints()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func addPerson(){

        self.container.addArrangedSubview(lbName)
        self.container.addArrangedSubview(imPerson)
        self.container.addArrangedSubview(tfValueFirstPerson)
        self.container.addArrangedSubview(btnSendValue)
        self.container.addArrangedSubview(lbValue)

        self.addSubview(container)
    }

    func addConstraints(){
        NSLayoutConstraint.activate([

            self.container.bottomAnchor.constraint(equalTo: self.bottomAnchor),

            self.container.topAnchor.constraint(equalTo: self.topAnchor),
            self.container.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            self.container.trailingAnchor.constraint(equalTo: self.trailingAnchor),

        ])
    }

The button action will do two things, so let's divide it into functions. Inside the @objc func setValueFirstPerson (_ button: UIButton) {}. We will call the function in which the textfield value is validated. If it is higher than the previous value it does something, if not, it does nothing.

To find out if the value of the textField is greater than the previous value, we are going to make a static variable because as we are creating three participants, we want to know which is the largest among the three.

static var maxValue = Int()

Let's do the function to validate func validateGreaterValueTf() -> Bool {}

@objc func setValueFirstPerson(_ button: UIButton){

        if(validateGreaterValueTf()){
            print("ok")
        }else{
            print("bad")
        }
    }

func validateGreaterValueTf() -> Bool {
    guard let value = self.tfValueFirstPerson.text else { return false}

    guard let valueInt = Int(value) else {return false}

    if(valueInt > Person.maxValue){
        Person.maxValue = Int(value)!
        return true
    }
    return false
}

We need to validate that the value that was written is from the same guy who sent it to the auctioneer, so let's compare the textField with the button using the name accessibilityIdentifier:

@objc func setValueFirstPerson(_ button: UIButton){

    guard let nameTf = self.tfValueFirstPerson.accessibilityIdentifier else { return }
    guard let nameButton = button.accessibilityIdentifier, let value = self.tfValueFirstPerson.text else { return }

    if(validateGreaterValueTf()){
        if(nameTf == nameButton){
            self.tfValueFirstPerson.text = ""
        }
    }else{
        self.tfValueFirstPerson.text = ""
    }
}

Note: note that we are clearing the textfield whether or not the validations are successful.

Depois de fazer as validações. Precisamos enviar esse valor para o leiloeiro e atualizar o valor de todos os participantes. Portanto, faremos o delegado para viewController.

protocol SendValueProtocol: class{
    func sendValue(text: String, indentify:String)
}
class Person: UIView {

    weak var delegate: SendValueProtocol?
}

End we will use the delegate in the button action:

@objc func setValueFirstPerson(_ button: UIButton){

    guard let nameTf = self.tfValueFirstPerson.accessibilityIdentifier else { return }
    guard let nameButton = button.accessibilityIdentifier, let value = self.tfValueFirstPerson.text else { return }

    if(validateGreaterValueTf()){
        if(nameTf == nameButton){
            self.tfValueFirstPerson.text = ""

            //Here     
            self.delegate?.sendValue(text: value, indentify: nameButton)  
        }
    }else{
        self.tfValueFirstPerson.text = ""
    }
}

We will pass the name of the participant that we call the button and the textfield value

ViewController.swift

class ViewController: UIViewController {
    let uiAuction = UIAuction()

    override func loadView() {

        self.view = uiAuction

    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.uiAuction.personOne.delegate = self
        self.uiAuction.personTwo.delegate = self
        self.uiAuction.personThree.delegate = self

    }
}

extension ViewController: SendValueProtocol{
    func sendValue(text: String, indentify: String) {

    }
}

In the viewController, we have to register our protocol for each instance of the participants and make it conform to the class.

To finish our application, we need to create a stopwatch, assign the value to all participants and auctioneer, through the observer, and also assign the winner.

See Post 2 to continue.