Computed (Nsobject) Properties in Swiftui Don't Update The View

Computed (NSObject) Properties in SwiftUI don't update the view

I don't see that NSObject is the source of the problem. The problem seems to be that you haven't implemented objectWillChange. The compiler will let you get away with that, but the result is that your objectWillChange does nothing.

Here's a simple example that shows how to configure an ObservableObject (that is an NSObject) with a computed property whose binding works:

class Thing : NSObject, ObservableObject {
let objectWillChange = ObservableObjectPublisher()
var computedProperty : Bool = true {
willSet {
self.objectWillChange.send()
}
}
}

struct ContentView: View {
@EnvironmentObject var thing : Thing
var body: some View {
VStack {
Button(self.thing.computedProperty ? "Hello" : "Goodbye") {
self.thing.computedProperty.toggle()
}
Toggle.init("", isOn: self.$thing.computedProperty).frame(width:0)
}
}
}

You can see by tapping the button and the switch that everything is live and responsive to the binding within the view.

Published computed properties in SwiftUI model objects

There are multiple issues here to address.

First, it's important to understand that SwiftUI updates the view's body when it detects a change, either in a @State property, or from an ObservableObject (via @ObservedObject and @EnvironmentObject property wrappers).

In the latter case, this is done either via a @Published property, or manually with objectWillChange.send(). objectWillChange is an ObservableObjectPublisher publisher available on any ObservableObject.

This is a long way of saying that IF the change in a computed property is caused together with a change of any @Published property - for example, when another element is added from somewhere:

elements.append(Talies())

then there's no need to do anything else - SwiftUI will recompute the view that observes it, and will read the new value of the computed property cumulativeCount.


Of course, if the .count property of one of the Tallies objects changes, this would NOT cause a change in elements, because Tallies is a reference-type.

The best approach given your simplified example is actually to make it a value-type - a struct:

struct Tallies: Identifiable {
let id = UUID()
var count = 0
}

Now, a change in any of the Tallies objects would cause a change in elements, which will cause the view that "observes" it to get the now-new value of the computed property. Again, no extra work needed.


If you insist, however, that Tallies cannot be a value-type for whatever reason, then you'd need to listen to any changes in Tallies by subscribing to their .objectWillChange publishers:

class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
@Published var elements: [Tallies] = [] {
didSet {
cancellables = [] // cancel the previous subscription
elements.publisher
.flatMap { $0.objectWillChange }
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}

private var cancellables = Set<AnyCancellable>

var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count } // no changes here
}
}

The above will subscribe a change in the elements array (to account for additions and removals) by:

  • converting the array into a Sequence publisher of each array element
  • then flatMap again each array element, which is a Tallies object, into its objectWillChange publisher
  • then for any output, call objectWillChange.send(), to notify of the view that observes it of its own changes.

Why won't my SwiftUI Text view update when viewModel changes using Combine?

For the time being, there is a solution that I luckily found out just by experimenting. I'm yet to find out what the actual reason behind this. Until then, you just don't inherit from NSObject and everything should work fine.

class ViewModel: ObservableObject {
@Published var str = ""
var count = 0

init() {

// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"

print("Updated count: \(String(describing: self?.count))")
}
}
}

I've tested this and it works.


A similar question also addresses this issue of the publisher object being a subclass of NSObject. So you may need to rethink if you really need an NSObject subclass or not. If you can't get away from NSObject, I recommend you try one of the solutions from the linked question.

@Published property wrapper not working on subclass of ObservableObject

Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.

NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>() on the subclass as that will again cause the state not to update properly.

I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.

Here is a fully working example:

    import SwiftUI

class MyTestObject: ObservableObject {
@Published var aString: String = ""

}

class MyInheritedObject: MyTestObject {
// Using @Published doesn't work on a subclass
// @Published var anotherString: String = ""

// If you add the following to the subclass updating the state also doesn't work properly
// let objectWillChange = PassthroughSubject<Void, Never>()

// But if you update the value you want to maintain state
// of using the objectWillChange.send() method provided by the
// baseclass the state gets updated properly... Jaayy!
var anotherString: String = "" {
willSet { self.objectWillChange.send() }
}
}

struct MyTestView: View {
@ObservedObject var myTestObject = MyTestObject()
@ObservedObject var myInheritedObject = MyInheritedObject()

var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")

TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}

Add @Published behaviour for computed property

Updated:
With the EnclosingSelf subscript, one can do it!

Works like a charm!

import Combine
import Foundation

class LocalSettings: ObservableObject {
static var shared = LocalSettings()

@Setting(key: "TabSelection")
var tabSelection: Int = 0
}

@propertyWrapper
struct Setting<T> {
private let key: String
private let defaultValue: T

init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}

var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}

public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
) -> T {
get {
return object[keyPath: storageKeyPath].wrappedValue
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
}
}
}

Updating SwiftUI view from an Objective C Class

Try the following

@objc
class BridgingClass: NSObject {

var baz = Foo() // here !!
...

and

struct ContentView: View {

@ObservedObject var baz = objectivec_class.bridgingClass.baz // << this !!

var body: some View {
Button(action: {
objectivec_class.updateSwiftUi()
})
{
Text(self.baz.bar)
}
}
}

Objectivec_Class.m

@implementation Objectivec_Class

- (id)init{
if( self = [super init] ){
_stringWhichWillBeRendered = [NSMutableString stringWithString:@""];
self.bridgingClass = [BridgingClass new]; // here !!

...

Change to @Published var in @EnvironmentObject not reflected immediately

Changing

final class UserData: NSObject, ObservableObject  {

to

final class UserData: ObservableObject  {

does fix the issue in Xcode11 Beta6. SwiftUI does seem to not handle NSObject subclasses implementing ObservableObject correctly (at least it doesn't not call it's internal willSet blocks it seems).



Related Topics



Leave a reply



Submit