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:
The structure of the project will look like this:
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.