Definition: A list of object interactions is encapsulated by a mediation object (mediator), which makes objects loosely coupled without explicitly referring to each other and can change their interactions independently.

The above definition of the mediator pattern seems to know the role of the mediator, but how to use it? So I’m going to work with my friends to implement a mediator.

Timers are often used in project development. We might write code like this:

class ViewController: UIViewController {

    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        timer = Timer.init(timeInterval: 1, target: self, selector: #selector(timeFire), userInfo: nil, repeats: true)RunLoop.current.add(timer! .forMode: .common)
        
    }

    @objc func timeFire() {
        
        print("time fire") } deinit { timer? .invalidate() timer = nilprint("The controller is destroyed.")}}Copy the code

Execute the code and the timer starts running. But when we leave the controller, we see that deinit is not executed. Because we pass self to the target of the Timer, we create a circular reference.

To solve this problem, we can initialize the Timer with block mode as follows:

timer = Timer.init(timeInterval: 1, repeats: true
    , block: { (timer) in
        
        print("timer fire")})Copy the code

This fixes the loop reference problem, but we’ll see that the block initialization Timer was not introduced until after iOS10, so it won’t work on iOS10. Even without this problem, But it’s also not an elegant way to override the deinit method every time to stop the timer.

Is there something else that can help me solve the timer loop references and stop the timer? This is the mediator pattern that we will introduce.

Since this is the mediator pattern, there must be a mediator, so we create a new class BOProxy that inherits from NSObject to act as the mediator.

class BOProxy: NSObject {
    
    weak var target: NSObjectProtocol?
    
    fileprivate var sel: Selector?
    
    fileprivate var timer: Timer?
    
    override init() {
        super.init()}}Copy the code

Because the mediator also needs to know information about the outside world, the responder target needs to be saved, and the weak modifier is used to break the circular reference. Use sel to save the method called by the timer. Use timer to save internally initialized timers.

We also add a sechduleTimer function to initialize the timer and receive incoming parameters.

func sechduleTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any? , repeats yesOrNo: Bool) {
    
    timer = Timer.init(timeInterval: ti, target: aTarget, selector: aSelector, userInfo: userInfo, repeats: yesOrNo)
    
    self.target = aTarget as? NSObjectProtocol
    
    self.sel = aSelector
    
    guardtarget? .responds(to: sel) ==true else {
        
        return
    }
    
    RunLoop.current.add(timer! , forMode: .common) }Copy the code

Also modify the code in controller:

proxy.sechduleTimer(timeInterval: 1, target: self, selector: #selector(timeFire), userInfo: nil, repeats: true)
Copy the code

It is then executed and the timer responds. However, after reverting to controller, the timer was not destroyed. And the circular reference still exists, is the mediator not working? Isn’t.

When we initialize the Timer in sechduleTimer, we directly use the passed target to initialize the Timer, so the circular reference is not broken, and the intermediary only acts as a wrapper, but does not play its due role.

Therefore, we should use BOProxy itself to initialize the Timer:

timer = Timer.init(timeInterval: ti, target: self, selector: aSelector, userInfo: userInfo, repeats: yesOrNo)
Copy the code

BOProxy does not implement the selector for timer callbacks. In order for BOProxy to own the external seletor, we use the Runtime method swap.

.RunLoop.current.add(timer! , forMode: .common)let method = class_getInstanceMethod(self.classForCoder, #selector(boTimeFire))!

class_replaceMethod(self.classForCoder, self.sel! , method_getImplementation(method), method_getTypeEncoding(method))Copy the code

Eventually, the timer calls back to boproxy.botimefire. We’re going to call target selector again in boTimeFire.

@objc fileprivate func boTimeFire(a) {
    
    if self.target ! =nil {
        
        self.target! .perform(self.sel)
    } else {
        
        self.timer? .invalidate()self.timer = nil}}Copy the code

At the same time, it determines whether the external target still exists and destroys the timer if it does not. The timer is automatically destroyed.

Execute the above code, the timer responds perfectly, exits the controller, and the controller is destroyed.

If you are a beginner developer, you can make it this far. But I’m a junior developer a little bit above the junior developer, so I might pass in BOProxy selector and say something like this:

let sel = NSSelectorFromString("timeFireNoIMP")

proxy.sechduleTimer(timeInterval: 1, target: self, selector: sel, userInfo: nil, repeats: true)
Copy the code

But timeFireNoIMP is not implemented, so what happens when you run it again? We see that there is no error, but the timer is not responding either.

And that’s because we know the target in the sechduleTimer, right? Responds (to: sel) To add a Timer to a RunLoop, it has to be true. But that’s not good, because you can’t find the cause of the error, it’s not easy to locate the error.

We can print a prompt when we determine that target does not implement sel.

guardtarget? .responds(to: sel) ==true else {
    
    print("\(sel!)Method not implemented")
    
    return
}
Copy the code

However, if you have too many logs in your project, it might not be easy to spot this hint. There is another way. I have always believed that if there is a bug, it is better to expose it directly during development/testing rather than after the APP is released. There’s nothing like a crash to expose a bug.

So, we just add the timer to the RunLoop, and we don’t do a selector check. So, if the selector isn’t implemented, it’s bound to crash the APP, but before it crashes it’s bound to go into message forwarding because of iOS’s fault tolerance. We will do the error prompt in the message forward, easy to locate the bug.

override func forwardingTarget(for aSelector: Selector!) -> Any? {
    
    if self.target? .responds(to: sel) ==true {
        
        return self.target
    } else {
        
        print("\(sel!)Method not implemented")
        
        return super.forwardingTarget(for: sel)
    }
}
Copy the code

Here I only do simple processing, interested partners can continue to expand, for example, in order to avoid crashes, you can use class_addMethod method in the message forwarding stage to add their own fault tolerance method.

This is a simple example of the mediator pattern. In RxSwift, mediators, such as sinks, are extensively used to deal with some inconvenient exposed methods and decouple the connections between objects. If there is any deficiency, please comment and correct.