swift ui handle property changed in observableObject - swift

so - I have a text field on a screen - the data loads asynchronously - so I've created and ObservableObject with a published field, and successfully bind it to the view:
class Blah : ObservableObject
{
#Published var value : Double? = nil
init()
{
load_variable_async().then {result in self.value = result }
}
}
which works perfectly - the view reflects the value of the variable and everything.
But - I want it to work both ways. Published seems to be a bidirectional wrapper, so I want to add something like this:
init()
{
load_variable_async().then {result in self.value = result }
value.when_changed { new_value in asynchronously_save( variable) }
}
and I can't find any way of doing it. Everything I google for puts a sink or some call to a save in the view - which seems completely wrong to me... if I'm reading it in one place, I want to be writing it in the same place - and if I'm already binding the variable to a textfield for instance, and bindings go both ways, then enough connections have already been made
So what am I doing wrong? How do react to value being set, inside my "model" object without explicitly putting some sort of save or other action into the view?

So I found the solution I wanted - the binding from model -> textfield was always working, but the value from textfield -> model I couldn't get working. The magic seems to be this keyword "willSet"
#Published var value : Double? = nil {
willSet( new_value ) {
print("going to save asynchronously now")
}}
works magically.

Related

Can you save the selection of a master-detail bound NSTableView?

I have a manager class for my data which is configured by two properties, one to set to a category and another to select items which correspond with that category. Based on that it will expose the relevant pieces of data. I am using a couple of different forms or making those selections, including a pair of IndexSets.
My problem is that I would also like to be able to save the selected items for each category, so that whenever the category is changed the items previously selected for it are restored. This is easy to achieve when accessed programmatically, but using bindings to allow a view in a macOS app to be able to provide that configuration unfortunately does not work properly
Changing the category causes the object bound to its selection to empty or 'preserve' the selected items before the category is actually updated. So the actual selection gets overwritten with, with noway I can see to tell the difference between this behaviour and a user action.
Here are the test code I have used for experimenting, with viewDidLoad generating some random test data to roughly mimic the structure o the real class. This does not attempt to save or restore the selection, but simply shows the overwriting behaviour.
class Thing: NSObject {
#objc dynamic var name: String
required init(name: String) {
self.name = name
}
}
class Stuff: NSObject {
#objc dynamic var name: String
#objc dynamic var things: [Thing]
required init(name: String, things: [Thing]) {
self.name = name
self.things = things
}
}
class StuffManager: NSObject {
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
print("THING: ", Array(thingsIndex))
}
}
}
class ViewController: NSViewController {
#objc dynamic var stuffManager = StuffManager()
override func viewDidLoad() {
super.viewDidLoad()
(1...10).forEach { stuffManager.things.append(Thing(name: "Thing \($0)")) }
(1...9).forEach {
let randomThings = Array(stuffManager.things.shuffled()[0...Int.random(in: 0..<10)])
stuffManager.stuff.append(Stuff(name: "Collection \($0)", things: randomThings))
}
stuffManager.stuff.append(Stuff(name: "Collection 10", things: []))
}
}
In Interface Builder I have a view containing an NSPopButton to select the Stuff, a multiple selection NSTableView to select the Things, and a pair of NSArrayControllers for each. The bindings are:
Stuff Array Controller
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.stuff
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.stuffIndex
Things Array Controller
Content Array:
Binding to: Stuff Array Controller, Controller Key: Selection, Model Key Path: things
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingIndex
The two interface objects are bound to these controllers in the standard way, the Content to the arrangedObjects and the Selection Indexes to the selectionIndexes of their respective array controller.
What this test code shows is that when the value in the popup button is changed the THING debug line appears before the STUFF debug line, that is it changes the selection of Things before it changes the Stuff. So any code in the property observer on stuffManager.things to save the new selection will save this change before being aware that the Stuff has changed.
Obviously this behaviour is to avoid the selection being made incorrect by the change to the content, or worse selecting out of bounds if the new content is shorter. But is there any way to detect when this is happening, rather than a user changing the selection? Or a way to override it to gain manual control over the process rather than having to accept the default behaviour of 'Preserve Selection' or the selection being cancelled if that option is disabled?
And what makes it more awkward is if this behaviour only occurs when the selection would change. If the selected Things exist for the new Stuff, or if nothing is selected, then nothing happens to trigger the property observer. Again this is understandable, but it prevents being able to cache the change and then only save the previous one if the Stuff has not changed.
I did wonder if using a separate IndexSet for each Stuff would avoid this problem, because then there would be no need for the NSTableView to manage the selection. I do not like the idea of keeping an IndexSet in the model but would accept it if it worked. But it does not. Again understandable, because the table view has no idea the Selection Indexes binding will be changed. Unless I am missing something?
But I tested this by updating the Stuff class to include the following:
#objc dynamic var selected = IndexSet() {
didSet {
print("THING: ", Array(selected))
}
}
Then changing the Selection Indexes binding of the Things Array Controller to:
Binding to: Stuff Array Controller, Controller Key: selection, Model Key Path: selected
Is what I am trying to achieve impossible? I would not have thought it that strange a thing to want to do, to save and restore a selection, but it seems impossible with bindings.
The only solution I can see is to forgo the master-detail style pattern and instead just maintain a separate [Thing] property in my data manager class, bind the Things Array Controller to this (or even just bind the table directly to the property), then whenever the popup button changes update the new property to match the stuff object.
Something like this in the StuffManager, with the table content bound to availableThings:
#objc dynamic var stuffIndex = IndexSet() {
didSet {
print("STUFF: ", Array(stuffIndex))
availableThings = stuff[stuffIndex.first!].things
}
}
#objc dynamic var availableThings = [Thing]()
It appears there is no way to prevent the NSTableView behaviour of automatically resetting its selection when the content changes. Nor any way to detect when this is happening, as it updates this before updating the selection on the NSPopupButton having changed. So here is how I have written the StuffManager class, adding a property for binding to the tableview so I can control the content changing:
class StuffManager: NSObject {
let defaults: UserDefaults = .standard
var canSaveThingsIndex = true
#objc dynamic var stuff = [Stuff]()
#objc dynamic var stuffIndex = IndexSet() {
didSet {
canSaveThingsIndex = false
if stuffIndex.count > 0 {
availableThings = stuff[stuffIndex.first!].things
let thing = stuff[stuffIndex.first!].name
if let items = defaults.object(forKey: thing) as? [Int] {
thingsIndex = IndexSet(items)
} else if availableThings.count > 0 {
thingsIndex = IndexSet(0..<availableThings.count)
} else {
thingsIndex.removeAll()
}
} else {
availableThings.removeAll()
thingsIndex.removeAll()
}
canSaveThingsIndex = true
}
}
#objc dynamic var things = [Thing]()
#objc dynamic var availableThings = [Thing]()
#objc dynamic var thingsIndex = IndexSet() {
didSet {
if canSaveThingsIndex && stuffIndex.count > 0 {
let thing = stuff[stuffIndex.first!].name
defaults.set(Array(thingsIndex), forKey: thing)
}
}
}
}
The Things Array Controller is now bound as:
Content Array:
Binding to: ViewController, Model Key Path: stuffManager.availableThings
Selection Indexes:
Binding to: ViewController, Model Key Path: stuffManager.thingsIndex
Though without being able to use the master-detail benefits of an NSArrayController they are not needed. Both the NSPopupButton and NSTableView can be bound directly to the StuffManager. And this allows the NSPopupButton's Selected Index can be bound to an Int int he Stuff Manager rather than needing to use an IndexSet despite multiple selections being impossible.
The main feature of the workaround is that because I am manually changing the content I can use the canSaveThingsIndex flag before changing the NSTableView content. So whenever its natural behaviour triggers the thingsIndex property observer, this can be ignored to prevent it overwriting the user's selection. It also avoids the unnecessary saving of a selection immediately after being restored.

SwiftUI: Sort list based on an #Published variable

I'm building a UI with SwiftUI, and I have an array that I use to build a List element. Now I want to sort that list based on a #Published variable coming from an #EnvironmentObject.
Approach 1
I tried getting the array already sorted, passing in the environment object to the sorting method:
List(getArraySorted(environmentObject)) { item in
//do stuff with item
}
This will compile, but the list will not update if environmentObject changes. I also tried passing in environmentObject.variableToBaseSortOn but to no avail.
Approach 2
I tried sorting the array inline in Swift UI:
List(array.sorted(by: { (lhs, rhs) -> Bool in
// do sorting based on self.environmentObject
})) { item in
// do stuff with item
}
This will compile, but crash:
Fatal error: No ObservableObject of type EnvironmentObjectClass found.
A View.environmentObject(_:) for EnvironmentObjectClass may be missing as an ancestor of this view.
The hint in the crash is incorrect, environmentObject is set and self.environmentObject is set to the correct object.
Your best approach is probably to sort the list within ObservableObject each time the data changes using a property observer, then manually informing the ObservableObject publisher of the change (rather than using #Published, so the unsorted array is not briefly published between changes.)
It's not a good idea to do the sorting directly in the body of the View because that block may be called many, many times as SwiftUI renders it (this is transparent to us as developers). I suspect that may be a reason that Approach 2 is not working.
For example:
import Combine
class SomeObject: ObservableObject {
// or whatever data type you need...
var array: [String] {
didSet {
array.sort(by: { (lhs, rhs) -> Bool in
// do sorting based on self.environmentObject
})
objectWillChange.send()
}
}
}

Is there an alternative to Combine's #Published that signals a value change after it has taken place instead of before?

I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.

Can't permanently change instance property from method?

Ok, I'm probably missing something super basic. I have an instance property called currentValue, initialized to be a String.
class ViewController: NSViewController {
var currentValue = ""
// ...
func getNewValue() {
currentValue = computeNewValue()
aLabel.stringValue = currentValue
}
func calledLater() {
println("\(currentValue)")
}
}
When I call getNewValue(), the label updates correctly.
But, when I call calledLater(), the currentValue is "reset" to an empty string.
At first I thought it was a weak storage thing but Swift apparently defaults to strong storage?
I tested this by initializing currentValue to "a" and, again, the label updates correctly, but when I get the variable later it returns "a".
I feel like I'm missing something but can't word it in a way that will let me do correct research.
You might be calling the methods on different instances of your 'ViewController' class.
(A typical scenario is when loading controllers from a storyboard; it "looks" like instances in the storyboard, but they are really just blueprints.)

Swift struct with 'Any' not copied correctly into a function

Here's my struct -
struct SettingsItem {
var id: String!
var defaultValue: Any!
init() {
}
}
Then it's being used -
var item2 = SettingsItem()
item2.id = "abcd"
item2.defaultVaule = "1234"
f(item2) // <-- breakpoint shows a good item
When executed, item looks good at the breakpoint shown above. But then inside function f, item is all messed up.
func f(item: SettingsItem) {
println(item) // <-- bad item!
}
It looks like item isn't copied correctly when calling f, but when I tried this on a playground it didn't reproduce.
Any ideas for what causes this?
Update
It seems to be working well when I change type of var defaultValue: Any! to anything else, like Int! or String!.
Also tried using a default constructor (removed my init()), didn't help.
Why does it fail to copy when using Any?
In Xcode 6.4 I get the same behaviour in a playground too.
Probably best not to rely on the built-in string-conversion functionality as that’s really only for debugging purposes. Instead, try giving your type an explicit Printable implementation:
extension SettingsItem: Printable {
var description: String {
// make this string whatever you think the appropriate
// string representation of your value is
return "{id: \(id), defaultValue: \(defaultValue)}"
}
}
If I add this, it now prints out this way inside f.
P.S. I’d suggest thinking about ways you can remove the ! and Any from your struct, they will lead to problems in the longer-term.