1
\$\begingroup\$

I have implemented a simple Observable class. I think that there is still room for improvement. Especially the fact that I have separate array to observe results and values what if I want to observe errors for example, I think that this solution can be improved to be scalable. Also I'm not sure who is responsible for the threading the class implementation or the caller.

the desired result is to have an interface that allows:

1) Observable declaration:

private let isLoadingObservable = Observable<Bool>(false)

2) Update value:

self.isLoadingObservable.value = true

3) Observe values changes:

override func viewDidLoad() {
    super.viewDidLoad()

    isLoadingObservable.observeValues(on: self) { isLoading in
        print(isLoading)
    } 
}

Also in case where failure is possible (network call for example) we can user Result :

1) Observable declaration:

private let dataObservable = Observable<[User]>([])

2) Update value:

dataObservable.result = Result.failure(URLError.badURL)

3) Observe result changes:

    dataObservable.observeResults(on: self) { result in
        switch result {
        case .success(let value):
            print(value)
        case .failure(let error):
            print(error)
        }
    }

where we have

struct User {
    let name: String
}

enum ServerError: Error {
    case invalidDataError
}

The implementation:

import Foundation

public enum Result<Value> {
    case success(Value)
    case failure(Error)

    var value: Value? {
        switch self {
        case .success(let value):
            return value
        case .failure:
            return nil
        }
    }
}


class ResultObserver<Value> {
    typealias ResultObserverBlock  = (Result<Value>) -> Void

    weak var observer: AnyObject?
    let block: ResultObserverBlock

    init(observer: AnyObject, block: @escaping ResultObserverBlock) {
        self.observer = observer
        self.block = block
    }
}

class ValueObserver<Value> {
    typealias ValueObserverBlock  = (Value) -> Void

    weak var observer: AnyObject?
    let block: ValueObserverBlock

    init(observer: AnyObject, block: @escaping ValueObserverBlock) {
        self.observer = observer
        self.block = block
    }
}

public class Observable<Value> {

    typealias ResultObserverBlock  = (Result<Value>) -> Void
    typealias ValueObserverBlock  = (Value) -> Void

    //MARK: - Private properties
    private var valueObservers  = [ValueObserver<Value>]()
    private var resultObservers = [ResultObserver<Value>]()

    //MARK: - Public properties
    public var result : Result<Value> {
        didSet {
            self.notifyObservers()
        }
    }

    public var value: Value? {
        get{
            return self.result.value
        }
        set {
            if let value = newValue {
                self.result = Result.success(value)
            }
        }
    }

    //MARK: - Struct lifecycle
    public init(_ result: Result<Value>) {
        self.result = result
    }

    public convenience init(_ value: Value) {
        self.init(Result.success(value))
    }

    public convenience init(_ error: Error) {
        self.init(Result.failure(error))
    }

    //MARK: - Observation
    func observeResults(on observer: AnyObject, observerBlock: @escaping ResultObserverBlock) {
        self.resultObservers.append(ResultObserver(observer: observer, block: observerBlock))
        observerBlock(result)
    }

    func observeValues(on observer: AnyObject, observerBlock: @escaping ValueObserverBlock) {
        self.valueObservers.append(ValueObserver(observer: observer, block: observerBlock))
        if let value = value {
            observerBlock(value)
        }
    }

    func remove(observer: AnyObject) {
        self.resultObservers = self.resultObservers.filter({$0.observer !== observer})
        self.valueObservers = self.valueObservers.filter({$0.observer !== observer})
    }

    //MARK: - Helpers
    private func notifyObservers() {
        for observer in self.valueObservers {
            if let value = value {
                observer.block(value)
            }
        }
        for observer in self.resultObservers {
            observer.block(result)
        }
    }
}
\$\endgroup\$
4
  • \$\begingroup\$ If Result is the only thing that you need from Alamofire then I would suggest to include the definition here directly, to make your code independent of an (otherwise unrelated) framework. \$\endgroup\$
    – Martin R
    Commented Jan 24, 2018 at 12:35
  • \$\begingroup\$ @MartinR done, hope that it's easier to be reviewed now \$\endgroup\$
    – iOSGeek
    Commented Jan 24, 2018 at 12:38
  • \$\begingroup\$ That does not compile, there are several errors (related to Result<Value> having too few parameters). – Also a (minimal) main program demonstrating the usage might be helpful. \$\endgroup\$
    – Martin R
    Commented Jan 24, 2018 at 12:42
  • \$\begingroup\$ @MartinR now it compiles and I have added usage information \$\endgroup\$
    – iOSGeek
    Commented Jan 24, 2018 at 13:13

2 Answers 2

2
\$\begingroup\$

There is quite a lot of "almost similar" code due to the fact that you treat "value observers" and "result observers" separately: Two classes

class ResultObserver<Value>
class ValueObserver<Value> 

with corresponding type aliases:

typealias ResultObserverBlock  = (Result<Value>) -> Void
typealias ValueObserverBlock  = (Value) -> Void

and instance variables

private var valueObservers  = [ValueObserver<Value>]()
private var resultObservers = [ResultObserver<Value>]()

three init methods, two observeResults() methods, etc.

This would be greatly simplified by just observing a generic type Value, which in particular can be a Result<>:

class Observer<Value> {
    typealias ObserverBlock  = (Value) -> Void

    weak var observer: AnyObject?
    let block: ObserverBlock

    init(observer: AnyObject, block: @escaping ObserverBlock) {
        self.observer = observer
        self.block = block
    }
}

public class Observable<Value> {

    //MARK: - Private properties
    private var observers  = [Observer<Value>]()

    //MARK: - Public properties
    public var value : Value {
        didSet {
            self.notifyObservers()
        }
    }

    //MARK: - Struct lifecycle
    public init(_ value: Value) {
        self.value = value
    }

    //MARK: - Observation
    func observe(on observer: AnyObject, observerBlock: @escaping Observer<Value>.ObserverBlock) {
        self.observers.append(Observer(observer: observer, block: observerBlock))
        observerBlock(value)
    }

    func remove(observer: AnyObject) {
        self.observers = self.observers.filter({ $0.observer !== observer })
    }

    //MARK: - Helpers
    private func notifyObservers() {
        for observer in self.observers {
            observer.block(value)
        }
    }
}

Note also the use of Observer<Value>.ObserverBlock in order to avoid defining the same closure type twice.

Now you can observe a simple (boolean) value:

private let isLoadingObservable = Observable(false)

// ...        

isLoadingObservable.observe(on: self) { isLoading in
    print("observed:", isLoading)
}

or a result:

private let dataObservable = Observable(Result<[User]>.success([]))

// ...

dataObservable.observe(on: self) { result in
    switch result {
    case .success(let value):
        print("Value:",  value)
    case .failure(let error):
        print("Error:", error)
    }
}

Another possible improvement could be to check in func notifyObservers() if the observing object still is alive, and remove it from the list otherwise.

\$\endgroup\$
0
\$\begingroup\$

One of the big draws of an Observable class are the operators like map, flatMap, and scan. At minimum you should implement those three functions.

That said, chaining your Observables together with map, & al. would be a bit problematic since they all would maintain a copy of the last element emitted (in the value parameter.)

I would rather see the Observable class do away with that parameter so I can chain several together without wasting a bunch of memory.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.