Lightweight Design Patterns in iOS (Part 1) - Observer

Design Patterns are part of Mobile Development for a while now and a revolution towards Quality Assurance is on. Such patterns have become a standard among the community, yet implementing an academic one might come with a price — complexity.

Because some Design Patterns solve very complex problems, they often result in a complicated implementation as well. So complex sometimes, that we tend to use heavy Third Party Frameworks, even for simple use cases.

The time has come to combine quality and simplicity.

🎬 Hi there, I’m Jean!

In this series, we are going to implement 4 of the most popular Design Patterns in a lightweight manner.
Today’s pattern is going to be…
🥁
…the Observer Pattern! 🎊

Observer Pattern


The Observation concept is getting more and more popular, mostly thanks to ReactiveX (Rx). 👀
Now, if all you need is subscribing to network calls while having a functional programming style, without the need for Coroutines, maybe importing the Rx library isn’t the best option… 🤔
But guess what: you can do it yourself, with no more than 💯 lines! 👍
(see for yourself here) 😜

So! Shall we?! 🧐

Define an Observable


To start, let’s create a Result enum.

enum Result<T> {
   case success(T)
   case failure(Error)
}

Then, let’s define the Observer type alias, which is no more than a closure taking a Result as parameter.

typealias Observer<T> = (Result<T>) -> Void

Simple so far, right? Moving on! 🏃‍

The next element we need is the Request enum for the NetworkManager.
It needs to contain a url, a method as well as parameters. ✅

enum Request: Hashable {
   case fetchSomething
   
   var url: String {
      switch self {
      case .fetchSomething:
         return "https://api.com/something"
      }
    }

    var method: HTTPMethod {
        switch self {
        case .fetchSomething
            return .get
        }
    }

    var parameters: Parameters {
       switch self {
       case .fetchSomething:
          return .url(["api-key": "abcd1234"])
       }
    }
}

Yaaaaaay, a fancy enum as we all love it! 🎉

But have you noticed? The enum Request is Hashable! 🤭
This means that a Request can be used as key to a Dictionary… 🤔
In this case, subscribe to a Request means that the Observer(s) will need to be grouped by Request. Which leads us to a Dictionary with Requests as keys and Observers as values: we are of course talking about the observables! 🤯

var observables: [Request: [Observer<Data>]]

Looking good huh?! 😎
“Only one thing missing”- you’ll tell me - “how do we subscribe to them?” 🤔

Subscribe to a Request


Looking now at the NetworkManager, its protocol is quite synthetic:
observe a Request with an Observer or simply execute a Request. 👌

protocol NetworkManagerProtocol {
   func observe(_ request: Request, _ observer: @escaping Observer<Data>)
   func execute(_ request: Request)
}

Nice and simple, all we want, is observe a Request, with a given Observer and in case of a pull-to-refresh action, simply executes a Request to notify the subscribed Observer(s)! 🚀

Alright, let’s implement it then! 💪

extension NetworkManager: NetworkManagerProtocol {
   
   func observe(_ request: Request, _ observer: @escaping Observer<Data>) {
      var observers = observables[request] ?? []
      observers.append(observer)
      observables[request] = observers
    
      execute(request, with: [observer])
  }
  
   func execute(_ request: Request) {
      guard let observers = observables[request] else {
         return
      }
    
      execute(request, with: observers)
   }
}

And that’s it! We’re done! ✅
We now can decode Data to Something in a dedicated Manager! 👌

func something(_ observer: @escaping Observer<Something>) {
   networkManager
      .observe(.fetchSomething) { result in
         ...
   }   
}

And use this Manager in the ViewController to update the view accordingly! 👍

override func viewWillAppear(_ animated: Bool) {
   super.viewWillAppear(animated)
  
   somethingManager
      .something { [weak self] result in
         ...
   }
}

Looks good! 😎
We got something pretty much looking like RxSwift there! 🙌
Now, don’t you see anything wrong in what we’ve done so far?
Have you ever encountered something called DisposeBag in Rx?
That’s right, with the way we implemented the Observer pattern, we might have memory leaks! 😱

I’m sure you know where I’m going, don’t ya’? 😛
Let’s implement a DisposeBag ourselves then! 💪

Dispose of an Observer


So, to dispose of an Observer, we first need to identify it, as so far it is simply stored in an array, remember? 🙂

How about having a Dictionary instead with unique identifiers as keys?! 😯

var observables: [Request: [UUID: Observer<Data>]]

Nice!
Now, an observable consists of a Request, which is observed by one or several Observer(s).
Each one of them, is uniquely identified with a UUID! 👏

Awesome, let’s change the NetworkManager a little bit… 😕

extension NetworkManager: NetworkManagerProtocol {
   
   func observe(_ request: Request, _ observer: @escaping Observer<Data>) {
      let uuid = UUID()
      var observers = observables[request] ?? [:]
      observers[uuid] = observer
      observables[request] = observers
      
      execute(request, with: [observer])
   }
  
   func execute(_ request: Request) {
      guard let observers = observables[request] else {
         return
      }
      
      execute(request, with: observers.map { $0.value })
   }
}

Not so bad finally, it looks almost the same! 😲
Alright, last piece of the puzzle, I promise! 😇

We need to implement a Disposable, so we can dispose of an Observer. 🗑

struct Disposable {
   let closure: () -> Void
}

Now, let’s make the DisposeBag! 👜

final class DisposeBag {
   var disposables: [Disposable] = []
  
   func dispose() {
      disposables.forEach { $0.closure() }
      disposables = []
   }
}

Finally, let’s bind those two together! 💞

extension Disposable {
   func disposed(by disposeBag: DisposeBag) {
      disposeBag.disposables.append(self)
   }
}

That was easy! 😎
Almost there, only one last small tweak in the NetworkManager and we’re finished! 🏁
Let’s now return the Disposable when observing a Request!

protocol NetworkManagerProtocol {
   @discardableResult
   func observe(_ request: Request, _ observer: @escaping Observer<Data>) -> Disposable
   ...
}
extension NetworkManager: NetworkManagerProtocol {

   @discardableResult
   func observe(_ request: Request, _ observer: @escaping Observer<Data>) {
      let uuid = UUID()
      var observers = observables[request] ?? [:]
      observers[uuid] = observer
      observables[request] = observers
    
      execute(request, with: [observer])
    
      return Disposable { [weak self] in
         self?.observables[request]?.removeValue(forKey: uuid)
      }
   }
   ...
}

And we’re finally done! 🙌

From now on, disposing an observable from the ViewController is ridiculously simple! 😆

let disposeBag = DisposeBag()

override func viewWillAppear(_ animated: Bool) {
   super.viewWillAppear(animated)
  
   somethingManager
      .something { [weak self] result in
         ...
   }.disposed(by: disposeBag)
}

override func viewWillDisappear(_ animated: Bool) {
   super.viewWillDisappear(animated)
  
   disposeBag.dispose()
}

And boom! 💥
We just made our own lightweight RxSwift-like Observer Pattern, without importing a single Framework! 📦

Was it worth it? Did that help you understand what problems the Observer Pattern is trying to solve?

If so, follow me on Twitter, I’ll be happy to answer any of your questions and you’ll be the first one to know when a new article comes out! 👌

See you next week, for the Part 2 of my series Lightweight Design Patterns in iOS!

Bye bye! 👋