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 owndidSet
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
Fbsdkapplicationdelegate Application Openurl:Sourceapplication:Annotation Deprecated
How to Concatenate Optional Swift Strings
With Firebase, Swift Removeobserver(Withhandle Does Not Remove the Observer
How to Repeat Animation (Using Uiviewpropertyanimator) Certain Number of Times
How to Resolve This Build Issue - Cannot Assign to Property: 'Date' Is a Get Only Property
How to Make Rounded Corner Progress Bar in Swift
Why Does My Version of Filter Perform So Differently Than Swifts
How Is a Return Value of Anyobject! Different from Anyobject
Target Parameter in Dispatchqueue
Nsbundle.Mainbundle().Urlforresource("Bach1", Withextension: "Jpg") Returning Null
Core Data: Could Not Cast Value of Type 'Mytype_Mytype_2' to Mytype
How to Code Initwithcoder in Swift
Subclass Nsapplication in Swift
Translucent Status Bar with No Navigation Bar
How to Set a Specific Default Time for a Date Picker in Swift
Pause an Skaction in Spritekit with Swift
How to Open Safari Extension Toolbaritem Popover Programmatically