The previous design Patterns (Swift) – 2. Singleton, Memo, and Policy patterns cover three common design patterns.

  • Singleton mode: limits class instantiation. A class can only instantiate one object. All references to singleton objects refer to the same object.
  • Memos: We can store an object locally and recover it at the appropriate time. The most common application in app development is the local cache of user data.
  • Policy mode: Encapsulates service branches to mask service details and provides only related policy interfaces as switches.

1. Observer Pattern

1. Overview of observer model

Observer mode: Changes in one object are known by another object. In addition to introducing the Runtime-based KVO implementation and its principles, this article will also try to implement a set of observer patterns on its own, since using Runtime in Swift is not recommended.

  • Subject: An observable used to be monitored.
  • Observer: Used to listen to the object being observed.

2. Realization of observer mode based on OC Runtime

1. Implement an observable inherited from NSObject
 // @objcMembers To add @objc keyword to every attribute in a class,
@objcMembers public class KVOUser: NSObject {
    dynamic var name: String

    public init(name: String) {
        self.name = name
    }
}
Copy the code

@objCMembers In order to add @objc to every property in a class, the properties of subclasses that inherit NSObject in Swift4 are not exposed to OC Runtime, so they have to be added manually

Swift itself is a static language. The @objc keyword is added to make properties dynamic. It can dynamically generate set and GET methods, because KVO needs to operate set methods.

    // Note the kvoObserver lifecycle
    var kvoObserver: NSKeyValueObservation?
    let kvoUser = KVOUser(name: "Dariel")

    // Listen for changes to the kvoUser name attribute
    kvoObserver = kvoUser.observe(\.name, options: [.initial, .new]) {
            (user, change) in
            print("User's name is \(user.name)")}Copy the code

The first parameter is the path, and this is the shorthand. Name, swift will convert itself to the full path; Options are NSKeyValueObservingOptions, here said the incoming initialization values and new values

2. Use observables that inherit from NSObject
 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      kvoUser.name = "John"
}
Copy the code

KvoObserver’s Observe method calls back whenever the name property of a kvoUser object is changed.

3. Realization principle of observer mode of OC Runtime

So how does KVO implement listening on object properties? When KVO is added to an object, the OC assigns the isa pointer to a subclass (NSKVONotifying_xxx) of the object to the Runtime, and the ISA pointer points to the original object Class object, and calls the set method in the class object to notify the listener which values have changed.

In Swift4, KVO is not supported at the language level, and to use it, you need to import Foundation and the observed object must inherit from NSObject, which is obviously not elegant.

4. Implement a non-Runtime observer mode

KVO’s observer mode is essentially a set method for getting attributes, which we can implement ourselves. Post the code directly to create an Observable swift file

public class Observable<Type> {
    
    // MARK: - Callback
    fileprivate class Callback {
        fileprivate weak var observer: AnyObject?
        fileprivate let options: [ObservableOptions]
        fileprivate let closure: (Type.ObservableOptions) - >Void
        
        fileprivate init(
            observer: AnyObject,
            options: [ObservableOptions],
            closure: @escaping (Type.ObservableOptions) - >Void) {
            
            self.observer = observer
            self.options = options
            self.closure = closure
        }
    }
    
    // MARK: - Properties
    public var value: Type {
        didSet {
            removeNilObserverCallbacks()
            notifyCallbacks(value: oldValue, option: .old)
            notifyCallbacks(value: value, option: .new)
        }
    }
    
    private func removeNilObserverCallbacks(a) {
        callbacks = callbacks.filter{$0.observer ! =nil}}private func notifyCallbacks(value: Type, option: ObservableOptions) {
        let callbacksToNotify = callbacks.filter{$0.options.contains(option) }
        callbacksToNotify.forEach { $0.closure(value, option) }
    }
    
    // MARK: - Object Lifecycle
    public init(_ value: Type) {
        self.value = value
    }
    
    // MARK: - Managing Observers
    private var callbacks: [Callback] = []
    
    
    /// Add an observer
    ///
    /// - Parameters:
    /// - observer
    /// -removeIFExists: Needs to be removed if an observer exists
    /// -options: observed
    /// - closure: callback
    public func addObserver(
        _ observer: AnyObject,
        removeIfExists: Bool = true,
        options: [ObservableOptions] = [.new],
        closure: @escaping (Type, ObservableOptions) -> Void) {
        
        if removeIfExists {
            removeObserver(observer)
        }
        
        let callback = Callback(observer: observer, options: options, closure: closure)
        callbacks.append(callback)
        
        if options.contains(.initial) {
            closure(value, .initial)
        }
    }
    
    public func removeObserver(_ observer: AnyObject) {
        callbacks = callbacks.filter{$0.observer ! == observer } } }// MARK: - ObservableOptions
public struct ObservableOptions: OptionSet {
    
    public static let initial = ObservableOptions(rawValue: 1 << 0)
    public static let old = ObservableOptions(rawValue: 1 << 1)
    public static let new = ObservableOptions(rawValue: 1 << 2)
    
    public var rawValue: Int
    
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}

Copy the code

Use:

public class User {
    // The observed property should be of type Observable
    public let name: Observable<String>
    public init(name: String) {
        self.name = Observable(name)
    }
}
// To manage the observer
public class Observer {}

var observer: Observer? // When the observer is set to nil, the observable is automatically freed.
let user = User(name: "Made")
observer = Observer() user.name.addObserver(observer! , options: [.new]) { name, changein     
    print("name:\(name), change:\(change)")}override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {        
    user.name.value = "Amel"
}
Copy the code

Note: If the value is changed during use and the addObserver method is not called, it is likely that the Observer has been freed.

5. Usage scenarios of observer mode

The observer pattern is commonly used in THE MVC pattern. The controller needs to listen for changes to a model property, and the model does not need to know the type of controller, so multiple controllers can listen for a model object.

2. Buidler Pattern

1. Overview of the Builder model

The Builder pattern can decompose the implementation of complex business scenarios step by step.

  • Dircetor: Usually used to manage builders, usually a Controller
  • Builder: Usually a class that manages the creation and data entry of products
  • Product: A more complex object, either a class or a structure, usually a model

1. Examples of builder patterns

1. Product
// MARK: - Product
public struct Person {
    public let area: Area
    public let character: Character
    public let hobby: Hobby
}
extension Person: CustomStringConvertible {
    public var description: String {
        return area.rawValue
    }
}
public enum Area: String { // from the region
    case ShangHai
    case ShenZhen
    case HangZhou
    case Toronto
}
public struct Character: OptionSet { / / character
    
    public static let independent = Character(rawValue: 1 << 1) / / 2
    public static let ambitious = Character(rawValue: 1 << 2) / / 4
    public static let outgoing = Character(rawValue: 1 << 3) / / 8
    public static let unselfish = Character(rawValue: 1 << 4) / / 16
    public static let expressivity = Character(rawValue: 1 << 5) / / 32

    public let rawValue: Int
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}
public struct Hobby: OptionSet { / / love
    
    public static let mountaineering = Hobby(rawValue: 1 << 1)
    public static let boating = Hobby(rawValue: 1 << 2)
    public static let climbing = Hobby(rawValue: 1 << 3)
    public static let running = Hobby(rawValue: 1 << 4)
    public static let camping = Hobby(rawValue: 1 << 5)
    
    public let rawValue: Int
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}
Copy the code

There are three attributes defined in Person: Area,character, and hobby. A region can only be one value, and a character and hobby can support multiple values. Character and Hobby can set multiple values by passing in a single value.

2. Builder
// MARK: - Builder
public class PersonStatistics {
    public private(set) var area: Area=.HangZhou
    public private(set) var characters: Character = []
    public private(set) var hobbys: Hobby = []
    
    private var outOfAreas: [Area] = [.Toronto]
    
    public func addCharacter(_ character: Character) {
        characters.insert(character)
    }
    
    public func removeCharacter(_ character: Character) {
        characters.remove(character)
    }
    
    public func addHobby(_ hobby: Hobby) {
        hobbys.insert(hobby)
    }
    
    public func removeHobby(_ hobby: Hobby) {
        hobbys.remove(hobby)
    }
    
    public func setArea(_ area: Area) throws {
        guard isAvailable(area) else { throw Error.OutOfArea }
        self.area = area
    }
    
    public func build(a) -> Person {
        return Person(area: area, character: characters, hobby: hobbys)
    }
    
    public func isAvailable(_ area: Area) -> Bool {
        return! outOfAreas.contains(area)
    }
    
    public enum Error: Swift.Error {
        case OutOfArea}}Copy the code

Manage Product uniformly through Builder, and create Person object after setting data.

3. Director
public class ManagerStatistics {

    public func createLiLeiData(a) throws -> Person {
        let builder = PersonStatistics(a)try builder.setArea(.HangZhou)
        builder.addCharacter(.ambitious)
        builder.addHobby([.climbing, .boating, .camping])
        return builder.build()
    }
    
    public func createLucyData(a) throws -> Person {
        let builder = PersonStatistics(a)try builder.setArea(.Toronto)
        builder.addCharacter([.ambitious, .independent, .outgoing])
        builder.addHobby([.boating, .climbing, .camping])
        return builder.build()
    }
}
Copy the code

Set the data in builder through Director.

 let manager = ManagerStatistics(a)if let Lucy = try? manager.createLucyData() {
     print(Lucy.description)
     print(Lucy.character)
     print(Lucy.hobby)
 }else {
     print("Out of area here")}if let Lilei = try? manager.createLiLeiData() {
     print(Lilei.description)
     print(Lilei.character)
     print(Lilei.hobby)
 }
Copy the code

2. Attention to the use of builder mode

Builder mode is used to create complex products that require a lot of values and are cumbersome to use constructors. If the Product is simple, use the constructor.

3. Summary

This article focuses on the observer pattern for listening on objects and the Builder pattern for creating and managing complex object scenarios.

The sample code

Reference:

The Swift Programming Language

The Tao of Objective-C programming

Design Patterns by Tutorials

If you have any questions, please leave a message at -d