Closure Identity in Swift: Unregister Observing Closure

Restrictions around Protocols and Generics in Swift

Your questions, 1 and 2, are in fact strongly related.

Before getting started though, I should point out that when you have first-class functions, the observable/observer pattern is almost entirely redundant. Instead of mandating interfaces for callbacks, you can just supply a closure. I’ll show this in the answer to question 2.

First, 1. The problem you are experiencing is type erasure. Your base class is the only place in which you have defined registerObserver and it looks like this:

class GenericObservable<EventType> {
private var observers:[GenericObserver] = []
func registerObserver(observer:GenericObserver) -> Bool {
// Code to avoid registering the same observer twice
observers.append(observer)
return true
}
//...
}

That is, it will take and store a protocol reference to any type. There is no constraint on what that type is, it can be anything. For example, you could notify an Int:

extension Int: GenericObserver {
func update<EventType>(observable:GenericObservable<EventType>, event:EventType) {
print("Integer \(self)")
}
}

numberObservable.registerObserver(2)

The problem will come when the callees try to use EventType. EventType could be anything. It is similar to this function:

func f<T>(t: T) { }

T can be any type you like – a String, an Int, a Foo. But you will not be able to do anything with it, because it provides zero guarantees. To make a generic type useful, you have to either constrain it (i.e. guarantee it has certain features, like that it’s an IntegerType that can be added/subtracted for example), or pass it on to another generic function that similarly unconstrained (such as put it in a generic collection, or call print or unsafeBitCast which will operate on any type).

Basically, your observers have all declared “I have a family of methods, update, which you can call with any type you like”. This is not very useful, unless you’re writing something like map or a generic collection like an array, in which case you don’t care what T is.

This might help clear up some confusion – this does not do what you think it does:

class DataObserver : GenericObserver {
func update<NSData>(observable:GenericObservable<NSData>, event:NSData) {
print("Data Event \(event)")
}
}

Here you have not declared that DataObserver specifically takes an NSData class. You’ve just named the generic placeholder NSData. Similar to naming a variable NSData – it doesn’t mean that is what the variable is, just that’s what you’ve called it. You could have written this:

class DataObserver : GenericObserver {
func update<Bork>(observable:GenericObservable<Bork>, event: Bork) {
print("Data Event \(event)")
}
}

Ok so how to implement an observable protocol with an associated type (i.e. a typealias in the protocol). Here’s an example of one. But note, there is no Observer protocol. Instead, Observable will take any function that receives the appropriate event type.

protocol Observable {
typealias EventType
func register(f: EventType->())
}
// No need for an "Observer" protocol

Now, let’s implement this, fixing EventType to be an Int:

struct FiresIntEvents {
var observers: [Int->()] = []

// note, this sets the EventType typealias
// implicitly via the types of the argument
mutating func register(f: Int->()) {
observers.append(f)
}

func notifyObservers(i: Int) {
for f in observers {
f(i)
}
}
}

var observable = FiresIntEvents()

Now, if we want to observe via a class, we can:

class IntReceiverClass {
func receiveInt(i: Int) {
print("Class received \(i)")
}
}

let intReceiver = IntReceiverClass()
// hook up the observing class to observe
observable.register(intReceiver.receiveInt)

But we can also register arbitrary functions:

observable.register { print("Unowned closure received \($0)") }

Or register two different functions on the same receiver:

extension IntReceiverClass {
func recieveIntAgain(i: Int) {
print("Class recevied \(i) slightly differently")
}
}

observable.register(intReceiver.recieveIntAgain)

Now, when you fire the events:

observable.notifyObservers(42)

you get the following output:

Class received 42
Unowned closure received 42
Class recevied 42 slightly differently

But with this technique, if you try to register a function of the wrong event type, you get a compilation error:

observable.register(IntReceiverClass.receiveString)
// error: cannot invoke 'register' with an argument list of type '(IntReceiverClass -> (String) -> ())

How to detect when UIView size is changed in swift ios xcode?

viewWillLayoutSubviews() and viewDidLayoutSubviews() will be called whenever the bounds change. In the view controller.

Xcode 13 warning for .swiftsourceinfo

How are you building your binary frameworks?
Generally, .swiftsourceinfo are not stable across different versions of Swift Compiler so shouldn't be included when distributing the framework (especially when BUILD_LIBRARY_FOR_DISTRIBUTION=YES). So they shouldn't be included in the .framework package. If you build your framework using xcodebuild archive it should not generate .swiftsourceinfo

Why does SwiftUI recall an existing view's body which contains stale data?

First, to answer my original question. The behavior is due to the way how ObservableObject (or EnvironmentObject) invalidates view. Both ListView and DetailView monitor data model changes by subscribing ObservableObject's objectWillChange publisher. Since it's impossible to control which subscriber receives change first, which view's body gets called is undefined. In practice, it's DetailView's body gets called first, that's the reason why the regular property in the detail view doesn't get updated.

Note that, while in this case we can give a concrete explanation and modify our code accordingly, in general it's unreliable to write code based on our understanding when a view's body gets called. SwiftUI may call a view's body at unexpected time. See this example. I believe when and how a view's body get called should be considered as SwiftUI implementation details. In other words, it's undefined.

Why is this an issue? Well, it's because when we call data model API in body's code (either view rendering code or intent code) we need to pass view specific data (e.g., regular properties of the view in the example code) as param to the API. Note while these view specific data are retrieved from data model, once they are saved in the view, they can go stale at any time. Since I use force unwrapping, it causes crash.

Why do I use force unwrapping? It's because I underestimated SwiftUI's complexity. I thought how views were invalidated are simple and predictable. I also thought when data model changed, the entire view hiearchy would be recreated first and then views with change would get their body called (and hence view specific data and data model were always consitent). It turned out both are wrong.

So, how to address the view specific data and data model inconsistency issue?

Approach 1: Using binding.

This is Apple's approach. See here. This approach looks great. But unfortunately it doesn't seem feasible in most practical applications, for at least two reasons:

  1. It only suits for simple data models. For complex data models (e.g. it contains bank accounts and transers which refers to each by id) and deep view hierarchies (e.g. account A detail view -> transfer view -> account B detail view -> ...), it's impossible to prepare all the data ahead, as a result it has to call data model API. BTW, I did experiment to pass both view specific data and data model as binding, it seemed to work but I ran into the second issue.

  2. Binding doesn't work well when being passed to deep view hiearchy. In my experiment it caused weird view identity issue. I'd like to cite @Asperi's comment on this (he has a lot of great answers on SwiftUI and he made the comment in a unrelated question).

Binding works bad being transferred into deep view hierarchy, use instead ObservableObject based view model to inject for each next view layer, with some communication between view model to update only needed properties.

Approach 2: Using the usual view model approach.

This requires validating params. There are different ways to do it:

  • Move the view specific data to data model object and encapsulte the validation in data model.

  • Do it in view rendering code in body.

Either way the data model api needs to check invalid params (that is, it shouldn't do force unwrapping).

Note the code in this approach doesn't look as simple as Apple's approach. Also it may require the use of if statement. This confused me a lot because it doesn't fell like the SwiftUI way Apple advertised. But based on my current understanding, it has to be this way to write practicial applications.


Since the question plagued me a lot, I want to emphasize it's an architecture requirement for data model API to be lenient, because a) a view's body can be called at unexpected time, b) hence there is no way to make sure view specific data and data model are consistent, c) hence data model API have to handle invalid params if we'd like to call it in body.

The situation in Apple's approach is a bit different. But as explained above, it only works in simple cases.



Related Topics



Leave a reply



Submit