This is a slightly more abstract version of this question. In the app, these nested Observable Objects are indeed used in a view (so I'd rather use Observable Objects rather than straight Publishers). However, I would like to be able to simply subscribe to the view models in order to test them. The protocol is there so I can mock out Nested in tests.
This is the basic setup:
protocol NestedProtocol: AnyObject {
var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
#Published var string = ""
}
class Parent: ObservableObject {
#Published var nested: NestedProtocol
init(nested: NestedProtocol) {
self.nested = nested
}
}
var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)
parent.$nested.sink { newValue in
print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)
Then this command
nested.string = "foo1" outputs "NEW VALUE ", which is expected as the initial value of nested. I would like it to output "NEW VALUE foo1". (TIL published variables seem to be current value publishers.)
Of course I could do
nested.string = "foo1"
parent.nested = nested
and I would get "NEW VALUE foo1", but that's smelly.
I tried
protocol NestedProtocol: ObservableObject {
var string: String { get set }
}
class Nested<T>: ObservableObject where T: NestedProtocol {
...
But in real life, I would like nested to declare some static constants, which is not allowed in generic types. So that doesn't work.
From the cited question/answer, I also tried combinations of
Parent
init() {
nested.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: sinkHole)
}
Nested
init() {
string.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: sinkHole)
}
No dice. Those methods were getting called but that outer-level sink was still just returning "NEW VALUE "
I also tried calling
parent.nested.string = "foo1"
So now I'm modifying the parent, and that should work, right? Wrong.
There's a bunch to unpack here.
First, you might know that if a property is a value-type, like a struct or String, then marking it as #Published just works:
class Outer {
#Published var str: String = "default"
}
let outer = Outer()
outer.$str.sink { print($0) }
outer.str = "changed"
Will output:
default
changed
Your question, however, is about a nested observable object, which is a reference type. So, the above wouldn't work with a reference-type.
But in your example you're using a protocol as an existential (i.e. in place of an eventual instance), and as you noted, without inheriting from AnyObject, then it really behaves like a value-type:
protocol InnerProtocol {
var str: String { get set }
}
class Inner: InnerProtocol {
#Published var str: String = "default"
}
class Outer {
#Published var inner: InnerProtocol
init(_ inner: InnerProtocol) { self.inner = inner }
}
let inner = Inner()
let outer = Outer(inner)
outer.$inner.sink { print($0.str) }
outer.inner.str = "changed"
This would also output:
default
changed
which looks like what you wanted, but in fact it doesn't really "observe" any changes in the nested object. When you do outer.inner.str, it has value-type semantics, so it's as-if you re-assigned the .inner property. But if you are truly interested in observing changes of the object itself, then this approach wouldn't work at all. For example:
nested.str = "inner changed"
would not cause an output. Neither would there be an output if the inner object changed its own property, e.g.:
init() {
DisplatchQueue.main.asyncAfter(.now() + 1) {
self.str = "async changed"
}
}
So, it's unclear what exactly you're trying to achieve. If you want to observe a reference type property, you'd need to observe it directly.
class Inner: ObservableObject {
#Published var str: String
//...
}
class Outer: ObservableObject {
var inner: Inner
//...
}
//...
outer.inner.$str.sink { ... }
// or
outer.inner.objectWillChange.sink { ... }
You can achieve this with a protocol too, if you insist:
protocol InnerProtocol: ObservableObject {
var str: String { get set }
}
class Inner: InnerProtocol {
#Published var str: String = "default"
}
class Outer<T: InnerProtocol>: ObservableObject {
var inner: T
init(_ inner: T) { self.inner = inner }
}
let inner = Inner()
let outer = Outer(inner)
outer.inner.$str.sink { ... }
inner.str = "changed"
This took me hours and I stumbled upon it by accident while trying to modify my protocol in various ways.
Lesson 1:
protocol NestedProtocol: AnyObject {
var string: String { get set }
}
should be
protocol NestedProtocol {
var string: String { get set }
}
Why? I'm not sure. Apparently, if the Parent cannot assume that the published object is a class, then it watches modifications on it more closely? My instinct tells me the exact opposite, but it goes to show how much I can trust my instinct.
Lesson 2:
Indeed my 4th idea was correct and you need to name the parent in the nested object modification:
nested.string = "foo1"
should be
parent.nested.string = "foo1"
Again, they're all classes so it goes slightly against my understanding, but I don't know all the magic that goes on under #Published.
The final complete version looks like this:
protocol NestedProtocol {
var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
#Published var string = ""
}
class Parent: ObservableObject {
#Published var nested: NestedProtocol
init(nested: NestedProtocol) {
self.nested = nested
}
}
var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)
parent.$nested.sink { newValue in
print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)
and
nested.string = "foo1"
parent.nested.string = "foo2"
returns
"NEW VALUE "
"NEW VALUE foo2"
Related
I've been becoming more familiar with the "copy on write" behavior of Swift structs. I think it's a really nice way to get around having to manage references for structs, but it's a bit cumbersome when dealing with deeply nested structures.
If you want to update a deeply nested value, you need a direct path to that value so you can modify it on a single line:
myStruct.nestedArray[index].nestedValue = 1
The compiler will copy myStruct.nestedArray[index] and set nestedValue to 1 on that new value. It will then copy myStruct.nestedArray and set the new value at index. It will then copy myStruct and replace the previous value with a new one that has all of the above changes.
This works just fine and it's pretty cool that you can do this with a single line of code without having to worry about anything that was referencing myStruct and its children before. However, if there is more complicated logic involved in resolving the path to the value, the logic becomes much more verbose:
struct MyStruct {
var nestedEnum: MyEnum
}
enum MyEnum {
case one([NestedStruct])
case two([NestedStruct])
}
struct NestedStruct {
var id: Int
var nestedValue: Int
}
var myStruct = MyStruct(nestedEnum: .one([NestedStruct(id: 0, nestedValue: 0)]))
if case .one(var nestedArray) = myStruct.nestedEnum {
if let index = nestedArray.firstIndex(where: { $0.id == 0 }) {
nestedArray[index].nestedValue = 1
myStruct.nestedEnum = .one(nestedArray)
}
}
Ideally you'd be able to do something like this:
if case .one(var nestedArray) = myStruct.nestedEnum {
if var nestedStruct = nestedArray.first(where: { $0.id == 0 }) {
nestedStruct.nestedValue = 1
}
}
But as soon as nestedStruct.nestedValue is set, the new value of nestedStruct is swallowed.
What would be nice is if Swift had a way to use inout semantics outside of functions, so I could take a "reference" to nestedArray and then nestedStruct within it and set the inner nestedValue, causing the copy to propagate back up to myStruct the same way as it would if I'd been able to do it in one line.
Does anyone have any nice ways to deal with deeply nested structs that might be able to help me out here? Or am I just going to have to put up with the pattern from my second example above?
The solution I ended up arriving at was pretty SwiftUI specific, but it may be adaptable to other frameworks.
Basically, instead of having a single top-level method responsible for deeply updating the struct, I arranged my SwiftUI hierarchy to mirror the structure of my struct, and passed Bindings down that just manage one node of the hierarchy.
For example, given my struct defined above:
struct MyStruct {
var nestedEnum: MyEnum
}
enum MyEnum {
case one([NestedStruct])
case two([NestedStruct])
}
struct NestedStruct {
var id: Int
var nestedValue: Int
}
I could do this:
struct MyStructView: View {
#Binding var myStruct: MyStruct
var body: some View {
switch myStruct.nestedEnum {
case .one: OneView(array: oneBinding)
case .two: TwoView(array: twoBinding)
}
}
var oneBinding: Binding<[NestedStruct]> {
.init(
get: {
if case .one(array) = myStruct.nestedEnum {
return array
}
fatalError()
},
set: { myStruct.nestedEnum = .one($0) }
)
}
var twoBinding: Binding<[NestedStruct]> { /* basically the same */ }
}
struct OneView: View {
#Binding var array: [NestedStruct]
var body: some View {
ForEach(0..<array.count, id: \.self) {
NestedStructView(nestedStruct: getBinding($0))
}
}
func getBinding(_ index: Int) -> Binding<NestedStruct> {
.init(get: { array[index] }, set: { array[index] = $0 })
}
}
struct NestedStructView: View {
#Binding var nestedStruct: NestedStruct
var body: some View {
NumericInput(title: "ID: \(nestedStruct.id)", value: valueBinding)
}
var valueBinding: Binding<Int> {
.init(get: { nestedStruct.value }, set: { nestedStruct.value = $0 })
}
}
The only annoying bit is that it can be a bit verbose to construct a Binding manually. I wish SwiftUI had some syntax for getting nested Bindings from a Binding containing an array or struct.
I am trying to get one object to listen to changes in the property of another object. I have it working as shown below, but I would prefer the observing object knew nothing of the Model, just the property.
class Model : ObservableObject{
#Published var items: [Int] = []
}
class ObjectUsingItems{
var itemObserver: AnyCancellable?
var items: [Int] = []
func observeItems(model: Model){
itemObserver = model.$items
.sink{ newItems in
self.items = newItems
print("New Items")
}
}
}
At the moment I begin observing the model.items as follows - which works:
let model = Model()
let itemUser = ObjectUsingItems()
itemUser.observeItems(model: model)
model.items.append(1) // itemUser sees changes
Unfortunately I can’t seem to figure out just what is required as the parameter to the observeItems method so that it works without knowing anything about the Model - like this:
class ObjectUsingItems{
var itemObserver: AnyCancellable?
var items: [Int] = []
func observeItems(propertyToObserve: WhatGoesHere?){
itemObserver = propertyToObserve
.sink{ newItems in
// etc.
}
}
}
And then call it like so:
itemUser.observeItems(XXX: model.$items)
Can anyone explain what I need to do? Thanks!
You can just accept a publisher as a parameter, if you don't care where the value comes from.
In your very specific case, it could be:
func observeItems(propertyToObserve: Published<[Int]>.Publisher) {
itemObserver = propertyToObserve
.sink { self.items = $0 }
}
But this might be too restrictive - why only this specific publisher? In principle, you shouldn't care what the publisher is - all you care about is the output value and error type. You can make it generic to any publisher, so long as its Output is [Int] and Failure is Never (like that of the #Published one):
func observeItems<P: Publisher>(propertyToObserve: P)
where P.Output == [Int], P.Failure == Never {
itemObserver = propertyToObserve
.sink { self.items = $0 }
}
The usage would be something like this:
let model = Model()
let itemUser = ObjectUsingItems()
itemUser.observeItems(propertyToObserve: model.$items)
I found a lot of SwiftUI-related topics about this which didn't help (eg Why an ObservedObject array is not updated in my SwiftUI application?)
This doesn't work with Combine in Swift (specifically not using SwiftUI):
class SomeTask {
#Published var progress = Progress(totalUnitCount: 5) // Progress is a Class
[...]
}
var task = SomeTask()
let cancellable = task.$progress.sink { print($0.fractionCompleted) }
task.progress.completedUnitCount = 2
This is not SwiftUI-related so no ObservableObject inheritance to get objectWillChange, but even if I try to use ObservableObject and task.objectWillChange.send() it doesn't do anything, also trying to add extension Progress: ObservableObject {} doesn't help.
Since the publisher emits values through the var's willSet and since Progress is itself class-type nothing happens.
Looks like there is no real decent way to manually trigger it?
Only solution I found is to just re-assign itself which is quite awkward:
let pr = progress
progress = pr
(writing progress = progress is a compile-time error).
Only other way which might be working is probably by using Key-value-observing/KVO and/or writing a new #PublishedClassType property wrapper?
I was able to implement this using KVO, wrapped by a #propertyWrapper, with a CurrentValueSubject as the publisher:
#propertyWrapper
class PublishedClass<T : NSObject> {
private let subject: CurrentValueSubject<T, Never>
private var observation: NSKeyValueObservation? = nil
init<U>(wrappedValue: T, keyPath: ReferenceWritableKeyPath<T, U>) {
self.wrappedValue = wrappedValue
subject = CurrentValueSubject(wrappedValue)
observation = wrappedValue.observe(keyPath, options: [.new]) { (wrapped, change) in
self.subject.send(wrapped)
}
}
var wrappedValue: T
var projectedValue: CurrentValueSubject<T, Never> {
subject
}
deinit {
observation.invalidate()
}
}
Usage:
class Bar : NSObject {
#objc dynamic var a: Int
init(a: Int) {
self.a = a
}
}
class Foo {
#PublishedClass(keyPath: \.a)
var bar = Bar(a: 0)
}
let f = Foo()
let c = f.$bar.sink(receiveValue: { x in print(x.a) })
f.bar.a = 2
f.bar.a = 3
f.bar.a = 4
Output:
0
2
3
4
The disadvantage of using KVO is, of course, that the key path you pass in must be #objc dynamic and the root of the keypath must be an NSObject subclass. :(
I haven't tried, but it should be possible to extend this to observe on multiple key paths if you want.
You can try using CurrentValueSubject<Progress, Never>:
class SomeTask: ObservableObject {
var progress = CurrentValueSubject<Progress, Never>(Progress(totalUnitCount: 5))
func setProgress(_ value: Int) {
progress.value.completedUnitCount = value
progress.send(progress.value)
}
}
var task = SomeTask()
let cancellable = task.progress.sink { print($0.fractionCompleted) }
task.setProgress(3)
task.setProgress(1)
This way your Progress can still be a class.
Based on the ideas I did implement a #PublishedKVO property wrapper and put it up on github as a small swift package, supporting multiple key paths.
https://github.com/matis-schotte/PublishedKVO
Usable as:
class Example {
#PublishedKVO(\.completedUnitCount)
var progress = Progress(totalUnitCount: 2)
#Published
var textualRepresentation = "text"
}
let ex = Example()
// Set up the publishers
let c1 = ex.$progress.sink { print("\($0.fractionCompleted) completed") }
let c1 = ex.$textualRepresentation.sink { print("\($0)") }
// Interact with the class as usual
ex.progress.completedUnitCount += 1
// outputs "0.5 completed"
// And compare with Combines #Published (almost°) same behaviour
ex.textualRepresentation = "string"
// outputs "string"
ex.$progress.emit() // Re-emits the current value
ex.$progress.send(ex.progress) // Emits given value
I have the following code:
class Note: NSObject {
}
struct Global {
static var notes: Array<Note> = [] {
didSet {
print("hi")
}
}
}
This prints "hi" if I add or remove an item from the array or if I do
Global.notes = []
Is there a way to print("hi") every time when one of the Note objects in the array is modified?
Thanks for your answers
Without changing the class to a struct, I have two basic ways to handle this.
This is the object you asked about
class Note: NSObject {
}
struct Global {
static var notes: Array<Note> = [] {
didSet {
print("hi")
}
}
}
Wrap Notes in a wrapper that is a struct to get the struct behavior.
extension Note {
struct Wrapper { let note: Note }
}
extension Global {
static var wrappedNotes = [Note.Wrapper]() {
didSet {
print("hi")
}
}
}
Global.wrappedNotes.append(Note.Wrapper(note: Note()))
Global.wrappedNotes[0] = Note.Wrapper(note: Note())
Global.wrappedNotes.remove(at: 0)
The other way is to create a note manager to wrap access to the array.
class NoteManager {
subscript(index: Int) -> Note {
get {
return values[index]
}
set {
defer { onUpdate() }
values[index] = newValue
}
}
func append(_ newNote: Note) {
defer { onUpdate() }
values.append(newNote)
}
func remove(at index: Int) -> Note {
defer { onUpdate() }
return values.remove(at: index)
}
private func onUpdate() {
print("hi")
}
private var values = [Note]()
}
extension Global {
static var managedNotes = NoteManager()
}
Global.managedNotes.append(Note())
Global.managedNotes[0] = Note()
Global.managedNotes.remove(at: 0)
As per #staticVoidMan comment , If you make your model , a struct, rather than a class, then the property observer didSet will work for your Note model's own properties as well.
import Foundation
struct Note {
var name: String
}
struct Global {
static var notes: Array<Note> = [] {
didSet {
print("hi")
}
}
}
Global.notes.append(Note(name: "Shubham"))
Global.notes.append(Note(name: "Bakshi"))
Global.notes[0].name = "Boxy"
This will print the following on the console :
hi
hi
hi
Swift Array is a struct, and structs are value-type which means they change completely when elements are added/removed/replaced. Hence when you add/remove/replace a Note, the didSet property observer gets called as the array has been set again.
However, as per you question:
Is there a way to print("hi") every time when one of the Note objects in the array is modified?
By this I am assuming that you want to do something when an element within this array is accessed and an internal property is modified.
This would have been fine if you were dealing with only value-type objects, i.e. had your Note object also been a struct, then changing anything inside one Note would have caused the array to change as well.
But your Note object is a class, i.e. reference-type, and stays as the same object even if it's internal elements change. Hence your array doesn't need to update and didSet does not get called.
Read: Value and Reference Types
KVO Solution:
Now... Since your Note is subclassing NSObject, you can use the KVO concept
As per the following working example, we observe only one property of the Note class.
If you want to observe more properties then you will need to observe those many more keypaths.
Example:
class Note: NSObject {
#objc dynamic var content = ""
init(_ content: String) {
self.content = content
}
}
class NoteList {
var notes: [Note] = [] {
didSet {
print("note list updated")
//register & save observers for each note
self.noteMessageKVOs = notes.map { (note) -> NSKeyValueObservation in
return note.observe(\Note.content, options: [.new, .old]) { (note, value) in
print("note updated: \(value.oldValue) changed to \(value.newValue)")
}
}
}
}
//array of observers
var noteMessageKVOs = [NSKeyValueObservation]()
}
let list = NoteList()
list.notes.append(Note("A")) //note list updated
list.notes.append(Note("B")) //note list updated
list.notes[0].content = "X" //note updated: A changed to X
list.notes[1].content = "Y" //note updated: B changed to Y
Notes:
NSObject is required for KVO
#objc dynamic is required to make a property observable
\Note.message is a keypath
noteMessageKVOs are required to keep the observers alive
I would like to create a class with a static property that subclasses can override, which would be used to initialize instances. So far, I've tried to accomplish this like this:
import Cocoa
class A: NSObject {
class var staticProperty: String {
return "A"
}
var property: String = A.staticProperty
}
class B: A {
override class var staticProperty: String {
return "B"
}
}
This does not work, since B().property still returns "A". How could I change this code so that property contains the value specified by the subclass? Any help would be appreciated!
Edit
I would like to initialize property with the value of staticProperty, so this could also look like this:
var property: SomeClass = SomeClass(A.staticProperty)
But then, this initialization should still use "A" for class A, and "B" for class B.
Edit 2 (After #RakeshaShastri's comment)
For my specific use-case, I need property to be stored (so not computed) and non-lazy.
Edit 3
In short, I'm trying to build a Realm model class which has a few to-many relationships to other models. For these models (which are quite similar), I'm trying to create a superclass which contains the shared functionality, amongst which is also the inverse relationship. Therefore, I want to have a static property which contains the key in the first model to either of the other models, and then initialize a LinkingObjects property using this key name. Since Realm does not allow this to be lazy or computed, I cannot use these functionalities here.
If you inherit from NSObject why not using it ?
import Cocoa
class A: NSObject {
var property: String
public override init() {
let str = type(of: self).perform(#selector(getter: type(of: self).staticProperty))?.takeUnretainedValue() as! String
property = str
}
#objc class var staticProperty: String {
return "A"
}
}
class B: A {
override class var staticProperty: String {
return "B"
}
}
You can do this with this aproach
class A {
var prop: String{
return "A"
}
}
class B: A {
override var prop: String{
return "B"
}
}
print(A().prop) // "PRINTS A"
print(B().prop) // "PRINTS B"
A.staticProperty will use static dispatch and will always point to A's class property. You need dynamic dispatch here, aka type(of: self).
However, self needs an instance to work with, thus var property: String = type(of: self.staticProperty won't compile.
However, lazy properties can work around this limitation, so you could declare property as a lazy one:
class A: NSObject {
class var staticProperty: String {
return "A"
}
private(set) lazy var property: String = { type(of: self).staticProperty }()
}
class B: A {
override class var staticProperty: String {
return "B"
}
}
print(B().property) // B
P.S. the private(set) part is just something I usually do, I rarely allow extrinsic factors to change my object.
Update As #MartinR has pointed out, lazy is not a choice for the OP. An alternative solution that doesn't use a lazy var is to use a "shadowing" property:
class A: NSObject {
class var staticProperty: String {
return "A"
}
private var _property: String?
var property: String {
get {
return _property ?? type(of: self).staticProperty
}
set {
_property = newValue
}
}
}
class B: A {
override class var staticProperty: String {
return "B"
}
}
let b = B()
print(b.property) // B
b.property = "B'"
print(b.property) // B'