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 itsobjectWillChange
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
Nspredicate in Query from Array Elements
How to Test Required Init(Coder:)
How to Cast from Uint16 to Nsnumber
Attrackingmanager Stopped Working in iOS 15
Type of Expression Is Ambiguous Without More Context in Xcode 11
File Couldn't Be Opened Because You Don't Have Permission to View It Error
Swift Struct with Lazy, Private Property Conforming to Protocol
How to Switch to Swift 4.0 in Xcode 9.3
How to 'Addtarget' to Uilabel in Swift
Codable Enum with Multiple Keys and Associated Values
Mandatory Init Override in Swift Uinavigationcontroller Subclass
Are Swift Constants Lazy by Default
Can a Condition Be Used to Determine the Type of a Generic
Add a Xib File to a Swift Package
Cannot Load Module Coredata as Coredata
Difference Between Optional Values in Swift
Spritekit: Howto Make Holes in Layer with Blendmode
Swiftui View and Uihostingcontroller in Uiscrollview Breaks Scrolling