Swift: How to Change a Property's Value Without Calling Its Didset Function

Swift: how to change a property's value without calling its didSet function

What you do in Objective-C to "avoid side effects" is accessing the backing store of the property - its instance variable, which is prefixed with underscore by default (you can change this using the @synthesize directive).

However, it looks like Swift language designers took specific care to make it impossible to access the backing variables for properties: according to the book,

If you have experience with Objective-C, you may know that it provides two ways to store values and references as part of a class instance. In addition to properties, you can use instance variables as a backing store for the values stored in a property.

Swift unifies these concepts into a single property declaration. A Swift property does not have a corresponding instance variable, and the backing store for a property is not accessed directly. (emphasis is mine)

Of course this applies only to using the "regular language" means, as opposed to using reflection: it might provide a way around this restriction, at the expense of readability.

How to call didSet without changing property value

Try this:

self.myProperty = { self.myProperty }()

How is didSet called again when setting inside through a function?

After checking with Swift github and asking questions about this problem, I find out that this problem is more complex as it seems. But there is a specific rule about this problem:

didSet observer will not trigger only if access to property within
its own didSet observer can be done through direct memory access.

Problem is that it is a little ambiguous when access to property will be direct(unless probably if you are developer of Swift). An important feature that has an effect on my question is this:

Class instance method never access class properties directly.

This quote shows problem with my code, even though I can argue that when an instance member should be able to access property directly whenever you call it in didSet observe. When I have a code like this:

class B {
var i = 0 {
didSet {
print("called")
doit()
}
}

func doit() {
self.i += 1
}
}

doit() function cannot access i directly which triggers didSet again causing infinite loop.

Now what is the workaround?

You can use inout for passing properties from its own didSet to a instance function without triggering didSet. Something like this:

class B {
var i = 0 {
didSet {
print("called")
doit(&i)
}
}

func doit(_ i: inout Int) {
i += 1
}
}

And one last thing. Starting Swift 5, conditions for selecting direct memory access for properties within its own didSet will become more restricted. Based on github, only conditions that will use direct memory access is are the following:

 Within a variable's own didSet/willSet specifier, access its storage
directly if either:
1) It's a 'plain variable' (i.e a variable that's not a member).
2) It's an access to the member on the implicit 'self' declaration.
If it's a member access on some other base, we want to call the setter
as we might be accessing the member on a *different* instance.

This means codes like following will trigger infinite loop while it does not right now:

class B {
var i = 0 {
didSet {
print("called")
var s = self
s.i += 1
}
}
}

In Swift, does resetting the property inside didSet trigger another didSet?

I also thought, that this is not possible (maybe it wasn't in Swift 2), but I tested it and found an example where Apple uses this. (At "Querying and Setting Type Properties")

struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// cap the new audio level to the threshold level
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// store this as the new overall maximum input level
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}

And below this piece of code, there is the following note:

In the first of these two checks, the didSet observer sets currentLevel to a different value. This does not, however, cause the observer to be called again.

Is there a way to get didSet to work when changing a property in a class?

Nothing happens because the observer is on test, which is a Foo instance. But you changed test.number, not test itself. Foo is a class, and a class is a reference type, so its instances are mutable in place.

If you want to see the log message, set test itself to a different value (e.g. a different Foo()).

Or, add the println statement to the other didSet, the one you've already got on Foo's number property.

Or, make Foo a struct instead of a class; changing a struct property does replace the struct, because a struct is a value type, not a reference type.

Swift - observe static member change, without using property observer

You could use NotifiationCenter by registering a notification and then calling it whenever you want to update something, like "updateConversations" in a chat app. For example in viewDidLoad(), register your notification:

NotificationCenter.default.addObserver(self, selector: #selector(self.updateConversations(notification:)), name: NSNotification.Name(rawValue: "updateConversations"), object: nil)

Add this function into the class:

@objc func updateConversations(notification: NSNotification) {
if let id = notification.userInfo?["id"] as? Int,
let message = notification.userInfo?["message"] as? String {
// do stuff
}
}

Use the notification from anywhere in your app:

let info = ["id" : 1234, "message" : "I almost went outside today."]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateConversationsList"), object: self, userInfo: info)

Swift - didSet property not called from binding changes

Use instead in main view .onChange(of:) modifier, like

  SomeView()
.onChange(of: buttonClicked) { _ in
self.needToCallMyFunction()
}

Update: variant for SwiftUI 1.0 / iOS 13+

import Combine   // needed to use Just

...

SomeView()
.onReceive(Just(buttonClicked)) { _ in
self.needToCallMyFunction()
}

How to call a method once two variables have been set

You could make your properties optional and check they both have values set before calling your function.

var varA: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}

var varB: String? = nil {
didSet {
if varA != nil && varB != nil {
myFunc()
}
}
}

Or you can call your function on each didSet and use a guard condition at the start of your function to check that both of your properties have values, or bail out:

var varA: String? = nil {
didSet {
myFunc()
}
}

var varB: String? = nil {
didSet {
myFunc()
}
}

func myFunc() {
guard varA != nil && varB != nil else { return }
// your code
}


Related Topics



Leave a reply



Submit