TCA: Using a reference-type property within a State struct fails to drive UI updates - swift-composable-architecture

Below is simplified State for a TCA App. (Assume the rest of the stack is compiling.)
//PROBLEM: When Dog is a Class, instead of a Struct, changes to the dog name property made in the Reducer fail to update the UI
class Dog: Equatable {
var name = "Spot"
//Equatable conformance
static func == (lhs: Dog, rhs: Dog) -> Bool {
lhs.name == rhs.name
}
}
struct DogManager: ReducerProtocol {
struct State {
var dog = Dog()
}
}
//Action enum and reduce method omitted
}
The question is why UI updates are not made when state changes are made in the reduce method to the dog property.
//in the 'reduce' method of a ReducerProtocol-conforming Struct
case .changeDogName: state.dog.name = "Fido"
//changing the property 'dog.name' (assuming 'dog' is a reference-type object)
//does *not* trigger view updates
Even when the Dog class is decorated with ObservableObject and the name property with the #Published property-wrapper, UI updates do not occur when dog.name is modified in the reduce method.
I understand the TCA library encourages us to put everything State-related into a Struct. But is it possible at all to use reference-type properties, and have SwiftUI updates work properly?
I know there are workarounds:
Make Dog a Struct, instead of a Class. Then everything works as expected.
Add Dog into a ReducerProtocol-conforming Struct as a separate ObservedObject property. Then UI updates also work as expected.
I'm just curious why UI updates don't work when a reference type is added as a property in a State struct, and then modified by the reducer. I would have thought that Equatable conformance by a Class would be sufficient to trigger view updates.

Related

Passing data to subview with Core Data and MVVM

I am using SwiftUI and Core Data with MVVM.
I have a ForEach loop and I want to pass the data to the subview. First I did this using a property like this:
#StateObject var viewModel = ListViewModel()
ForEach(viewModel.items) { item in
NavigationLink {
ItemDetailView() // empty view
} label: {
ItemListRowView(name: item.name!)
}
}
Then in the subview ListRowView would be something like:
let name: String
Text("\(name)")
And the view model where the ForEach is grabbing its data:
#Published var items: [ItemEntity] = []
#Published var name: String = ""
func getItems() {
let request = NSFetchRequest<ItemEntity>(entityName: "ItemEntity")
do {
items = try dataManager.context.fetch(request)
} catch let error {
print("\(error.localizedDescription)")
}
}
That works as expected but now I want to edit the data and pass more properties to the subviews. I think this means I need to use bindings and #ObservedObject in the subviews.
What I see commonly done is one would make a custom Item data type conforming to Identifiable protocol, for example:
struct Item: Identifiable {
let id: UUID
let name: String
}
And then they'd update their ForEach to use the Item type and do something like let items: [Item] = [] but I've already got let items: [ItemEntity] = [] with ItemEntity being the name of the Core Data Item entity.
What I suspect needs to happen is in my getItems method, items needs to be changed to use an Item data type. Is this correct? Or how should I go about this? I'm shiny new to Core Data and MVVM and any input will be super appreciated.
Edit: I've seen this done too but I'm not sure if it's what I'm looking for:
ForEach(viewModel.items.indicies) { index in
SubView(viewModel.items[index])
}
Couple of mistakes:
ForEach is a View, not a loop, if you attempt to use it with indices it will crash when you access an array by index in its closure. In the case of value types you need to supply the ForEach with an id which needs to be a property of the data that is a unique identifier. Or the data can implement Identifiable. However, in the case of objects like in Core Data, it will automatically use the object's pointer as its id, which works because the managed object context ensures there is only one instance of an object that represents a record. So what this all means is you can use ForEach with the array of objects.
We don't need MVVM in SwiftUI because the View struct is already the view model and the property wrappers make it behave like a view model object. Using #StateObject to implement a view model will cause some major issues because state is designed to be a source of truth whereas a traditional view model object is not. #StateObject is designed for when you need a reference type in an #State source of truth, i.e. doing something async with a lifetime you want to associate with something on screen.
The property wrapper for Core Data is #FetchRequest or #SectionedFetchRequest. If you create an app project in Xcode with core data checked the template will demonstrate how to use it.

Dynamically retrieving data of different types based on user input using Swift

New to Swift, I am trying to build an iOS app that allows a user to construct dynamic reports. I have created several model classes meant to represent various types of data to report on, e.g. Widgets and Gadgets, and associated service classes for these models containing the logic needed to retrieve their data. Examples for a model and service here:
class Widget: BaseModel, Equatable, Identifiable {
let id: UUID
let value: Int
let timeStamp: Date
init(rawData: RawData) {
self.value = rawData.quantity
self.timeStamp = rawData.date
self.id = UUID()
}
func toString() -> String {
return "\(self.value) widgets"
}
static func == (lhs: Widget, rhs: Widget) -> Bool {
return lhs.id == rhs.id
}
}
class WidgetService: BaseService {
let queryService: QueryService = QueryService()
func fetchSamplesInDateRange(dates: (startDate: Date, endDate: Date)?) async -> [Widget] {
// get samples and return them
}
}
These are based off of two protocols with associated types, BaseModel and BaseService. What I'm trying to do in this very early proof of concept stage is implement a simple view that will allow a user to select a type of data (corresponding to one of my Models) and run a query for all data for that Model within a date range. But I am completely stuck on how to implement the model selection.
I have a basic view file I've been working with that just has one button that executes a fetch function, populating a state variable results.
func fetchData() async -> Void {
let modelService: some BaseService = // not sure what to do here
let result: [some BaseModel] = await modelService.fetchSamplesInDateRange(dates: nil)
results = result
}
Because BaseModel and BaseService have associated types they can't be used in the way above as type constraints, I know, I put them there to give an idea of what I'm trying to do.
I had thought to put the class names as strings, choosable via options in a Picker, and then use something like NSClassFromString to get the right class for the service, but ultimately this just doesn't seem workable as I have to declare the concrete service class type at some point, this solution just moves around where I run into the Protocol 'BaseModel' can only be used as a generic constraint because it has Self or associated type requirements error.
Any idea what I'm missing here? This sort of thing using base classes, extensions, generics, just doesn't seem like it should be this difficult as other languages handle it fine (though admittedly with less type safety).

Binding model and view: how to observe object properties

I have a view structured like a form that creates a model object. I am trying to bind the form elements (UIControl) to the model properties, so that the views auto-update when their corresponding model property is changed, and the model update when the controls are changed (two way binding). The model can change without the view knowing because multiple views can be linked to one same model property.
Approach 1: Plain Swift
My problem is the following: to observe changes to the model properties, I tried to use KVO in Swift, and specifically the observe(_:changeHandler:) method.
class Binding<View: NSObject, Object: NSObject, ValueType> {
weak var object: Object?
weak var view: View?
var objectToViewObservation: NSKeyValueObservation?
var viewToObjectObservation: NSKeyValueObservation?
private var objectKeyPath: WritableKeyPath<Object, ValueType>
private var viewKeyPath: WritableKeyPath<View, ValueType>
init(betweenObject objectKeyPath: WritableKeyPath<Object, ValueType>,
andView viewKeyPath: WritableKeyPath<View, ValueType>) {
self.objectKeyPath = objectKeyPath
self.viewKeyPath = viewKeyPath
}
override func bind(_ object: Object, with view: View) {
super.bind(object, with: view)
self.object = object
self.view = view
// initial value from object to view
self.view![keyPath: viewKeyPath] = self.object![keyPath: objectKeyPath]
// object --> view
objectToViewObservation = object.observe(objectKeyPath) { _, change in
guard var view = self.view else {
// view doesn't exist anymore
self.objectToViewObservation = nil
return
}
guard let value = change.newValue else { return }
view[keyPath: self.viewKeyPath] = value
}
// view --> object
viewToObjectObservation = view.observe(viewKeyPath) { _, change in
guard var object = self.object else {
// object doesn't exist anymore
self.viewToObjectObservation = nil
return
}
guard let value = change.newValue else { return }
object[keyPath: self.objectKeyPath] = value
}
}
}
However some of the properties of my model have types CustomEnum, CustomClass, Bool?, and ClosedRange<Int>, and to use observe I had to mark them as #objc dynamic, which yielded the error:
Property cannot be marked #objc because its type cannot be represented in Objective-C
Approach 2: Using RxSwift rx.observe
I turned to RxSwift and the rx.observe method thinking I could work around this problem, but the same thing happened (at runtime this time).
// In some generic bridge class between the view and the model
func bind(to object: SomeObjectType) {
object.rx
.observe(SomeType.self, "someProperty")
.flatMap { Observable.from(optional: $0) }
.bind(to: self.controlProperty)
.disposed(by: disposeBag)
}
Approach 3: Using RxSwift BehaviorRelays?
This is my first experience with RxSwift, and I know I should be using BehaviorRelay for my model, however I don't want to change all my model properties as my model object is working with other framework. I could try to implement a bridge then, to transform model properties into BehaviorRelay, but I would come across the same problem: how to listen for model changes.
In this question, there were no answer as to how to listen for property changes without refactoring all model properties to RxSwift's Variable (currently deprecated).
Approach 4: Using didSet Swift property observer?
The didSet and willSet property observers in plain Swift would allow listening for changes, however this would require to mark all the properties in the model with these observers, which I find quite inconvenient, since my model object has a lot of properties. If there is a way to add these observers at runtime, this would solve my problem.
I believe that what I am trying to achieve is quite common, having a set of views that modify a model object, however I can't find a way to properly link the model to the view, so that both auto-update when needed.
Basically, I'm looking for an answer to one of the following questions:
Is there something I overlooked, is there a better way to achieve what I want?
or How to overcome the "Property cannot be marked #objc" problem?
or How to bridge my model object to BehaviorRelay without changing my model?
or How to add didSet observers at runtime?
You said:
I believe that what I am trying to achieve is quite common, having a set of views that modify a model object, however I can't find a way to properly link the model to the view, so that both auto-update when needed.
Actually it's not at all common. One idea you don't mention is to wrap your entire model into a Behavior Relay. Then the set of views can modify your model object.
Each of your views, in turn, can observe the model in the behavior relay and update accordingly. This is the basis of, for example, the Redux pattern.
You could also use your approach #3 and use property wrappers to make the code a bit cleaner:
#propertyWrapper
struct RxPublished<Value> {
private let relay: BehaviorRelay<Value>
public init(wrappedValue: Value) {
self.relay = BehaviorRelay(value: wrappedValue)
}
var wrappedValue: Value {
get { relay.value }
set { relay.accept(newValue) }
}
var projectedValue: Observable<Value> {
relay.asObservable()
}
}
But understand that the whole reason you are having this problem is not due to Rx itself, but rather due to the fact that you are trying to mix paradigms. You are increasing the complexity of your code. Hopefully, it's just a temporary increase during a refactoring.
Old Answer
You said you want to make it "so that the views auto-update when their corresponding model property is changed, and the model update when the controls are changed (two way binding)."
IMO, that way of thinking about the problem is incorrect. Better would be to examine each output independently of all other outputs and deal with it directly. In order to explain what I mean, I will use the example of converting °F to °C and back...
This sounds like a great reason to use 2-way binding but let's see?
// the chain of observables represents a view model
celsiusTextField.rx.text // • this is the input view
.orEmpty // • these next two convert
.compactMap { Double($0) } // the view into an input model
.map { $0 * 9 / 5 + 32 } // • this is the model
.map { "\($0)" } // • this converts the model into a view
.bind(to: fahrenheitTextField) // • this is the output view
.disposed(by: disposeBag)
fahrenheitTextField.rx.text
.orEmpty
.compactMap { Double($0) }
.map { ($0 - 32) * 5 / 9 }
.map { "\($0)" }
.bind(to: celsiusTextField.rx.text)
.disposed(by: disposeBag)
The above code handles the two-way communication between the text fields without two-way binding. It does this by using two separate view models (The view model is the code between the text Observable and the text Observer as described in the comments.)
We can see a lot of duplication. We can DRY it up a bit:
extension ControlProperty where PropertyType == String? {
func viewModel(model: #escaping (Double) -> Double) -> Observable<String> {
orEmpty
.compactMap { Double($0) }
.map(model)
.map { "\($0)" }
}
}
You may prefer a different error handling strategy than what I used above. I was striving for simplicity since this is an example.
The key though is that each observable chain should be centered on a particular effect. It should combine all the causes that contribute to that effect, implement some sort of logic on the inputs, and then emit the needed output for that effect. If you do this to each output individually you will find that you don't need two-way binding at all.

KVO not working for custom property of NSManagedObject

I have a subclass of NSManagedObject Folder with a state of Availability
#objc enum Availability: Int16 {
case unknown
case available
case unavailable
}
Folder has to do extra stuff (like delete related files) whenever it's availability changes. So I have
internalAvailability saved in core data
Computed property availability using above property
`
extension Folder {
#NSManaged private var internalAvailability: Availability
}
extension Folder {
private func deleteFiles(...) {
...
}
#objc dynamic public var availability: Availability {
get {
return internalAvailability
}
set {
willChangeValue(forKey: "availability")
deleteFiles()
internalAvailability = newValue
didChangeValue(forKey: "availability")
}
}
}
Using Reactive, I want to change navigation item's title based on availability but the signal is never called after once!
```
let property = DynamicProperty<NSNumber>(object: folder, keyPath: "availability")
internalVariable = property // To have a reference of property
navigationItem.reactive.title <~ property.map { (stateNumber) -> String in
guard let a = Availability(rawValue: stateNumber.int16Value) else {
assertionFailure()
return ""
}
let prefix = a == .available ? "" : "(Nope) "
return "\(prefix)\(folder.name)"
}
I have explicitly added KVO compliance to the property in hopes that this starts working, but alas no results.
Edit: if I create the DynamicProperty on internalAvailability instead of availability, everything works smoothly..
Adding as an answer since it became a learning exercise. Hopefully someone else too would be benefitted.
The app uses a multiple managedObjectContext(moc) architecture. 1 private moc to make changes and 1 main thread moc that synchronises itself using mergeChanges.
In above code, navigationItem is using the folder instance kept with main-moc. The DynamicProperty is listening to KVO changes on this main-moc's folder instance. Let's call this main-folder. When I make changes, I modify the folder instance we have on private-moc. Let's call it private-folder.
On modifying private-folder and calling save on private-moc, a notification of name NSManagedObjectContextDidSave is broadcasted. main-moc synchronizes itself using mergeChanges.
mergeChanges changes main-folder, but notice that it would never call the computed-property-setter availability. It directly changes internalAvailability.
And thus, no KVO notifications are posted of our computed property.
TL;DR When doing KVO on a NSManagedObject subclass, use a stored property instead of computed one. In case you have a multi-moc (managed object context) scenario and use mergeChanges to synchronise, setter for your computed property is not called when synchronising.
Edit (Solution): add method of the pattern keyPathsForValuesAffecting<KeyName> KVO relevant documentation
#objc class func keyPathsForValuesAffectingAvailability() -> Set<NSObject> {
return [#keyPath(Folder.internalAvailability) as NSObject]
}
When using Core Data we use the NSManagedObjectContextObjectsDidChange notification instead of KVO. This brings many advantages including coalescing of change events and undo support. If we need to know what attributes changed on an object we can examine changedValuesForCurrentEvent which even includes transient attributes that have a matching keyPathsForValuesAffecting.... These advantages likely outweigh those from a KVO binding framework aka reactive.

Swift: property observers for computed properties

As far as I know, Swift allows us to set property observers for either stored and computed properties. But if computed property value depends on some backing store, property observers are not fired when these backing store values are changed:
public class BaseClass {
private var privateVar1: Int = 0
private var privateVar2: Int = 0
public var property: Int {
get {
return privateVar1 * privateVar2
}
set {
print("some setter without effect")
}
}
private func changeSomeValues() {
privateVar1 = 1
privateVar2 = 2
}
}
public class SubClass : BaseClass {
override var property: Int {
didSet {
print("didSet \(property)")
}
}
}
didSet of SubClass isn't called when changeSomeValues is called.
Let's consider a case: we have such BaseClass in a third-party framework. We define SubClass in our app. The question is: how can we rely on SubClass observers without knowledge about property nature: is it stored (and we can rely on observers) or computed (and then we can't expect firing observers each time when we expect it)? Is it possible? If no, is it an incapsulation violation?
That behaviour is perfectly normal. There is no way for the compiler to know which backing store really corresponds to which computed property. Your backing store in this case is made up of private variables that will not be accessible outside the class itself. So the only place where an "under the hood" change can occur is in the base class. It is that class's prerogative to use its calculated properties (which will trigger the observers) or the backstore (which will not).
In your example, assuming you never want to allow "invisible" changes, the changeSomeValues() function is breaking its own rules and not respecting the contract it promised to its subclasses and callers.

Resources