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)
Related
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"
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 am building a function that will take an instance of a class object and convert it to an XML request to be sent to a web service. To accomplish this, I am using mirroring to iterate through the key/value pairs in the class. In my testing, I see it is working great, with one major problem, none of the inherited class parameters are coming across. For example, in the code below, the loop is executed three times for "descriptionText, modelNumber and serialNumber, name and uuid are never collected. Is there a way for me to use mirroring and pick up all the parameters of the base class, as well as the widget? Also, if there is a better way to do this, I am all ears.
import UIKit
class BaseObject: NSObject {
var name = String()
var uuid = String()
}
class Widget: BaseObject {
var descriptionText = String()
var modelNumber = String()
var serialNumber = String()
}
var widget1 = Widget()
widget1.name = "Generic Widget"
widget1.uuid = "A guid"
widget1.descriptionText = "Class A Extra Larget Widget"
widget1.modelNumber = "1234"
widget1.serialNumber = "4321"
let widgetMirror = Mirror(reflecting: widget1)
for attr in widgetMirror.children {
print(attr.label!)
}
You need to use the superclassMirror: Mirror? property for that. For instance:
for attr in widgetMirror.superclassMirror!.children {
print(attr.label!)
}
prints the expected results:
name
uuid
Update. If you keep running into this, add this Mirror extension to your toolkit:
extension Mirror {
var allChildren: [Mirror.Child] {
var allChildren: [Mirror.Child] = []
var mirror: Mirror! = self
repeat {
allChildren.append(contentsOf: mirror.children)
mirror = mirror.superclassMirror
} while mirror != nil
return allChildren
}
}
Usage:
for attr in widgetMirror.allChildren {
print(attr.label!)
}
A partial answer was already posted, but here is a way to wrap it all up.
var mirror: Mirror? = Mirror(reflecting: widget1)
repeat {
for (index, child) in mirror!.children.enumerated() {
print (child.label!)
}
mirror = mirror?.superclassMirror
} while mirror != nil
I thought about a bit more. I think this is more Swifty.
extension Mirror {
var posterity: Children {
if let superclassMirror = superclassMirror {
return Children([children, superclassMirror.posterity].joined())
} else {
return children
}
}
}
let widgetMirror = Mirror(reflecting: widget1)
for child in widgetMirror.posterity {
print(child.label!)
}
I am experiencing an error Contextual type AnyObject cannot be used within dictionary literal in that func addPet down there, while trying to populate database with the newPet argument constituents in a dictionary.
import Foundation
import Firebase
struct Pet {
var name:String?
var type:String?
var age:Int?
var feedingList:[String]
var walkingList:[String]
}
struct User {
var currentId:String?
var numberOfPets:Int?
var pets:[Pet]
}
class petBrain {
var reff = FIRDatabaseReference()
var currentUser:User = User(currentId: "",numberOfPets: 0,pets: [])
init(){
self.reff = FIRDatabase.database().reference()
}
func setUserId(cId:String?){
self.currentUser.currentId = cId
}
func addPet(newPet:Pet) {
self.reff.child("pets").childByAutoId().setValue(["name":newPet.name, "type":newPet.type, "age":newPet.age, "fList":newPet.feedingList, "wList":newPet.walkingList])
}
}
I have already done this in other viewController, similarly for users and its working fine in dictionary shape (producing no error)
let em = emailTextField.text!
let us = usernameTextField.text!
...
else {
print("User created! Loging in.")
self.login()
// adding user to DB of users
self.ref.child("users").child(user!.uid).setValue(["email":em, "username":us])
}
What did i do wrong in the pets case? its maybe due to struct, or struct element types? Are those two structs well defined?
? is used if the value can become nil in the future.
! is used if it really shouldn't become nil in the future, but it needs to be nil initially.
See the problem is Swift is a strictly typed language, if you declare a variable ? you are saying that it's of type nil. So a dictionary cant tell what type of a value would it be storing....
var myVar : String? // Look myVar, you and your type is completely nil as of now
var myVar : String! // Look myVar, your value is nil as of now but your type is certainly of String
Just change your code to this:-
struct Pet {
var name:String!
var type:String!
var age:Int!
var feedingList:[String]
var walkingList:[String]
}
struct User {
var currentId:String?
var numberOfPets:Int?
var pets:[Pet]
}
class petBrain {
var reff = FIRDatabaseReference()
var currentUser:User = User(currentId: "",numberOfPets: 0,pets: [])
init(){
self.reff = FIRDatabase.database().reference()
}
func setUserId(cId:String?){
self.currentUser.currentId = cId
}
func addPet(newPet:Pet) {
let dict = ["name":newPet.name, "type":newPet.type, "age":newPet.age, "fList":newPet.feedingList, "wList":newPet.walkingList]
self.reff.child("pets").childByAutoId().setValue(dict)
}
}