Sunday, December 23, 2018

Observer pattern in Swift

Implementing an observer pattern in Swift can be tricky in Swift because Swift has automatic garage collection. This can lead to "retain cycles". What this means is that the observable has a reference to the observer and the observer has a reference to the observable, then neither object will be reclaimed, even if both objects go out of scope.

You can read about retain cycles here
Naturally, publishers of events are going to be running on a background thread and will be publishing events from the background thread. Please note that the below sample code is not suitable if your observers need to update the GUI. If you need to update the GUI, please change the dispatch mechanism to publish on the main thread.
Here is my solution

The key to the solution is to keep a weak reference in an array, indexed by the objectID of the observer in the observable. When we need to publish a notification to the observers, we check to see if the observer is still valid. If not, we remove it from the array, otherwise we send the notification.
 
// ------------------------------------------
//      MyObserverProtocol
// ------------------------------------------
protocol MyObserverProtocol: class {
    func onEvent();
}



//--------------------------------------------
//      EventGenerator class
// -------------------------------------------
class EventGenerator : NSObject
{
    // declare an internal structure which holds the weak reference to the
    // observer.
    struct ObserverRef{
        weak var m_Observer: MyObserverProtocol?;
    }
    
    // declare an dictionary of ObserverRef's. The key is an ObjectIdentifier, which is the
    // unique identifier assigned to every object automatically by Swift.
    private var m_Observers = [ ObjectIdentifier: ObserverRef ]();
    
    //---------------------------------------
    func addObserver(_ observer: MyObserverProtocol) {
        let id = ObjectIdentifier(observer);
        self.m_Observers[id] = ObserverRef( m_Observer:observer );
        print("Added Observer with \(id) into observers array...");
    }
    
    //---------------------------------------
    func removeObserver(_ observer: MyObserverProtocol) {
        let id = ObjectIdentifier(observer);
        self.m_Observers.removeValue(forKey: id);
    }

    
    //---------------------------------------
    func notifyObservers(){
        print("Event genrator called on thread \(Thread.current.debugDescription)");
        for (id, aObserver ) in self.m_Observers {
            if let observer = aObserver.m_Observer {
                observer.onEvent();
            }
            else{
                // observer did hara-kiri. remove it from the array
                print("EventGenerator: Observers with id: \(id) has died, removing from my observers list");
                self.m_Observers.removeValue(forKey: id);
            }
        }
    }
}


let m_SleepSec:UInt32 = 5;

// Declare a global observer...
let m_EventGenerator: EventGenerator = EventGenerator();


// -------------------------------------------
//  Simple observer class. 
// -------------------------------------------
class SomeObserver : MyObserverProtocol {
    let m_Name: String;
    
    init( name : String){
        self.m_Name = name;
    }
    
    func onEvent() {
        print("Yaay! I am \"\(m_Name)\" and I got a notification..." );
    }
}

// Global observer, should exist till the lifetime of the program.
let observer1: SomeObserver = SomeObserver( name: "Observer - 1" );
m_EventGenerator.addObserver(observer1)


// Simple testto check if the observer gets removed from the notifiers
func runTest1(){
    let observer2: SomeObserver = SomeObserver( name:"Observer - 2");
    m_EventGenerator.addObserver(observer2)
    //generate an asynchronous event..
    DispatchQueue.global(qos: .utility).async {
        // Asynchronous code running on the low priority queue
        m_EventGenerator.notifyObservers();
    }
    print("runTest1: Tests are running, main-thread going to sleep for \(m_SleepSec) sec ");
    sleep( m_SleepSec );     // 5 seconds. Enough time (hopefully) for the OS to schedule the task we set up above
    print("RunTest1 exiting..");
}

runTest1();

// generate an asynchronous event. This time, we should have only one observer, observer1
DispatchQueue.global(qos: .utility).async {
    // Asynchronous code running on the low priority queue
    m_EventGenerator.notifyObservers();
}
print("Tests are running, main-thread going to sleep for \(m_SleepSec) sec ");
sleep( m_SleepSec );
print("Main thread done..");