Skip to content

Add queue support and keypath support to ObserverSet#21

Open
Jon889 wants to merge 1 commit intotheappbusiness:developfrom
Jon889:jon-update-observer-set
Open

Add queue support and keypath support to ObserverSet#21
Jon889 wants to merge 1 commit intotheappbusiness:developfrom
Jon889:jon-update-observer-set

Conversation

@Jon889
Copy link
Copy Markdown

@Jon889 Jon889 commented May 27, 2020

We currently have some extension methods that add support for specifying the queue that callback is called, this integrates it more nicely and makes some other changes.

Also I changed the indentation to use tabs instead of spaces as it's more accessible.

I'll update the Example app if this is acceptable. We use it extensively in the Tesco App and after these changes all our Unit tests pass.

Comment thread ObserverSet.swift
self.entries = self.entries.filter{ $0.observer !== observer }
}
}
private class ObserverSetEntryImpl<ObserverType: AnyObject, Parameters>: ObserverSetEntry<Parameters> {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to name with ObserverSetEntry and have the above class called AnyObserverSetEntry, but naming it this way is more backwards compatible.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we name this something like DefaultObserverSetEntry instead? Impl doesn't read very well in my opinion

Comment thread ObserverSet.swift

- returns: An entry in the list of observers, which should be used later to remove the observer.
*/
open func add(queue: DispatchQueue? = nil, _ callBack: @escaping (Parameters) -> Void) -> ObserverSetEntry<Parameters> {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure about this but it makes sense that this shouldn't be @discardableResult to prompt the user to remember to remove the observer at some point. If they really want it to be always added then they can do _ = observerSet.add(...) which I've had to do in some places in the Tesco App where we set up observers on app launch.

Comment thread ObserverSet.swift
- returns: An entry in the list of observers, which can be used later to remove the observer.
*/
@discardableResult
open func add<ObserverType: AnyObject>(queue: DispatchQueue? = nil, _ lifeCycleObserver: ObserverType, _ callBack: @escaping (Parameters) -> Void) -> ObserverSetEntry<Parameters> {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this function for convenience when you want to use a closure but want to tie to the lifecycle of an object.

One use case is instead of this:

let someObserverSet = ObserverSet<Int>()
let someObserverSet = ObserverSet<String>()

class Foo {
  private let observer1: Any
  private let observer2: Any
  init() {
      // Use closures to ignore the parameters that are different
      observer1 = someObserverSet.add { [weak self] _ in self?.update() }
      observer2 = anotherObserverSet.add { [weak self] _ in self?.update() }
  }
  func update() {
  
  }
  deinit {
    someObserverSet.remove(observer1)
    someObserverSet.remove(observer2)
  }
}

Just do this:

class Foo {
  init() {
      // Use closures to ignore the parameters that are different
      someObserverSet.add(self) { [weak self] _ in self?.update() }
      anotherObserverSet.add(self) { [weak self] _ in self?.update() }
  }
  func update() {
  
  }
}

An alternative would be to allow observers that have no parameters for any ObserverSet that has a non void Parameters type, but that would remove some of the type safety.

class Foo {
  init() {
      // Use closures to ignore the parameters that are different
      someObserverSet.add(self, Foo.update)
      anotherObserverSet.add(self, Foo.update) // oops I meant to do Foo.bar which takes the parameter but there is no error because any () -> Void function can be used as a callback for any ObserverSet
  }
  func update() {
  
  }

  func bar(_: String) { }
}

Comment thread ObserverSet.swift
*/
open func removeObserver<ObserverType: AnyObject>(_ observer: ObserverType) {
synchronized {
self.entries.removeAll(where: { ($0 as? ObserverSetEntryImpl<ObserverType, Parameters>)?.observer === observer })
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched to removeAll(where:) instead of filter because it reads better

Comment thread ObserverSet.swift
- returns: An entry in the list of observers, which can be used later to remove the observer.
*/
@discardableResult
func add<ObserverType: AnyObject>(queue: DispatchQueue? = nil, _ observer: ObserverType, _ callBack: @escaping (ObserverType) -> () -> Void) -> ObserverSetEntry<Parameters> {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed for the other adds as Swift seems to realise () -> Void and (Void) -> Void are the same thing

Comment thread ObserverSet.swift
/**
Removes an observer from the list.
fileprivate func call(_ parameters: Parameters) {
// overridden
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think its worth putting an assertionFailure in here to indicate that it should be subclassed and overridden?

assertionFailure("Function should be overridden before being called")

Comment thread ObserverSet.swift
Comment on lines +55 to +64
if let observer = observer {
let callBack = self.callBack(observer)
if let queue = queue {
queue.async {
callBack(parameters)
}
} else {
callBack(parameters)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor. But might be cleaner to guard around observer rather than have these nester if let statements.

Suggested change
if let observer = observer {
let callBack = self.callBack(observer)
if let queue = queue {
queue.async {
callBack(parameters)
}
} else {
callBack(parameters)
}
}
guard let observer = observer else { return }
let callBack = self.callBack(observer)
if let queue = queue {
queue.async {
callBack(parameters)
}
} else {
callBack(parameters)
}

Copy link
Copy Markdown

@jsanderson44 jsanderson44 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Jon889. Sorry it's taken so long for someone to review this. It looks good from my perspective, just a couple of minor suggestions. Ive asked a couple of others to review as well before we merge.

Comment thread ObserverSet.swift
}
}

fileprivate var isExpired: Bool { false }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current swift version for this repo is 4.0 so these one line implicit returns wont work yet.

Suggested change
fileprivate var isExpired: Bool { false }
fileprivate var isExpired: Bool { return false }

Comment thread ObserverSet.swift
private let callBack: ObserverCallback
private let queue: DispatchQueue?

override var isExpired: Bool { observer == nil }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Suggested change
override var isExpired: Bool { observer == nil }
override var isExpired: Bool { return observer == nil }

@roger-tan
Copy link
Copy Markdown

Hi @Jon889 , Your PR looks great. is there a chance to get the Unit Testing fixed?

To fix the issue with Xcode 12, I updated the initializers of TestObserver with the code below that and this should resolve the issue. It might also help to migrate to Swift 5 in future.

init(observee: TestObservee) {
    observee.voidObservers.add(self, voidSent)
    observee.stringObservers.add(self, stringChanged)
    observee.twoStringObservers.add(self, twoStringChanged)
    observee.intObservers.add(self, intChanged)
    observee.intAndStringObservers.add(self, intAndStringChanged)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants