The behavior of property observers surprises me in the following situation. I understand that observers do not recursively call themselves, but this behavior seems to carry over to different instances of the same class, and even to instances of different subclasses.
As I understand it, property observers run any time a property is set, even if the value doesn't change, except in initialization. What exactly are the exceptions to this rule? When exactly will property observers be disregarded as seen below?
var observersCalled = 0
class ClassOne {
var relatedOne: ClassOne?
var relatedTwo: ClassTwo?
var property: String = "Initial" {
didSet {
observersCalled += 1
relatedOne?.property = property
relatedTwo?.property = property
}
}
}
class ClassTwo {
var property: String = "Initial" {
didSet {
observersCalled += 1
}
}
}
class Subclass: ClassOne {
override var property: String {
didSet {
observersCalled += 1
}
}
}
let thing = ClassOne()
thing.relatedOne = ClassOne()
thing.property = "New Value"
print(observersCalled) //1 (really?)
observersCalled = 0
thing.relatedOne = nil
thing.relatedTwo = ClassTwo()
thing.property = "Another Value"
print(observersCalled) //2 (makes sense)
observersCalled = 0
thing.relatedOne = Subclass()
thing.relatedTwo = nil
thing.property = "Yet Another Value"
print(observersCalled) //1 (really!?)
There are no exceptions. The behavior I asked about is a bug. I have submitted a bug report for those tracking its progress.
Related
Given this two classes:
class A: UIView{
var setMe: Int!
#IBInspectable var setMePlease: Int = 0{
didSet{
setMe = setMePlease
}
}
}
class B: A{
#IBInspectable var control: Bool = false{
didSet{
let a = setMe + 1
}
}
}
It will crash if you first set the value of control and than setMePlease in the interface builder. Why does the didSet listen rather to the time the value is inserted than the class hierarchy? Is there a way to let the values listen to the hierarchy?
The issue is that setMe is defined as an implicitly unwrapped optional and if you set control before a value is assigned to setMe in the property observer of setMePlease, the value of setMe will be nil, causing a crash due to the force unwrapping.
You should either declare setMe as a simple optional if a nil value actually makes sense or rather provide it a default value.
class A: UIView{
var setMe: Int?
#IBInspectable var setMePlease: Int = 0{
didSet{
setMe = setMePlease
}
}
}
class B: A{
#IBInspectable var control: Bool = false{
didSet{
if let setValue = setMe {
let a = setMe + 1
} else {
let a = 1
}
}
}
}
Property observers are not called when assigning a default value during initialization, this is why setMe is nil when accessing it from the property observer of control.
This phenomenon can be easily seen using the following code, namely the property observer is only called after programatically changing the value of the variable, it is not called when assigning the default value to it:
class Parent{
var aCopy:Int?
var a = 1 {
didSet{
aCopy = a
}
}
}
class Child: Parent {
var b = 0{
didSet{
a = 2
let c = (aCopy ?? 0) + b
}
}
}
let child = Child()
child.aCopy //nil
child.a //1
child.b = 1 //b is assigned a new value, so its property observer is called
child.a //2
child.aCopy //2
In my model, I have a singleton class which will contain some global properties and methods. I think I've set up the class correctly but I need a way to verify incoming data for the properties. I'm trying to use get and set but these seem to need to return void. I can't use init because it's a singleton.
Am I missing something?
final class Globals {
private init(){}
static let sharedInstance = Globals()
//MARK: Properties
private var _peopleCount: Int!
var peopleCount: Int! {
get {
return _peopleCount
}
set(newPeopleCount) {
guard newPeopleCount > 0 else {
return nil // can't return nil here
}
}
}
}
You shouldn't define your variables as implicitly unwrapped optionals unless you have a very good reason to do so.
Your immediate error is that you cannot return a value in a setter, you need to assign the value to the variable there. If you want to mark an invalid value by peopleCount being nil, define peopleCount as Int? and assign to it nil when the check fails.
final class Globals {
private init(){}
static let sharedInstance = Globals()
//MARK: Properties
private var _peopleCount: Int?
var peopleCount: Int? {
get {
return _peopleCount
}
set(newPeopleCount) {
if let newValue = newPeopleCount, newValue > 0 {
_peopleCount = newValue
}
}
}
}
For most use cases, there is no need for the private backing variable, you can just use didSet to check the value before assigning it. Thanks for #LeoDabus for the idea in comments.
var peopleCount: Int? {
didSet {
if let newValue = peopleCount, newValue > 0 {
peopleCount = newValue
} else {
peopleCount = oldValue
}
}
}
I want to know how I can do the following:
var x = Observed<String>("hello")
label.text = x
Somewhere else x is updated:
x.value = "World"
Then label.text is automatically updated. Here is what I have for Observed so far
class Observed<T> {
typealias Observer = (T)->()
var observer: Observer?
var value: T {
didSet {
if let observer = observer {
observer(value)
}
}
}
init(_ v: T) {
value = v
}
}
I believe this is sort of what RxSwift, etc. do, but that's overkill for what I'm doing
You should probably just use a didSet in this scenario.
class YourClass {
var x: String = "" { didSet { someVC.someLabel.text = x } }
}
There is no "native" data binding in iOS as of now.
EDIT: of course there is KVO, thanks #matt for the heads-up :)
I have a property
public lazy var points: [(CGFloat,CGFloat,CGFloat)] = {
var pointsT = [(CGFloat,CGFloat,CGFloat)]()
let height = 100.0
for _ in 1...10 {
pointsT.append((someValue,someValue,100.0))
}
return pointsT
}()
And i want to add a didSet method, is it possible?
Short answer: no.
Try out this simple example in some class or method of yours:
lazy var myLazyVar: Int = {
return 1
} () {
willSet {
print("About to set lazy var!")
}
}
This gives you the following compile time error:
Lazy properties may not have observers.
With regard to the let statement in the other answer: lazy variable are not necessary just "let constants with delayed initialisation". Consider the following example:
struct MyStruct {
var myInt = 1
mutating func increaseMyInt() {
myInt += 1
}
lazy var myLazyVar: Int = {
return self.myInt
} ()
}
var a = MyStruct()
print(a.myLazyVar) // 1
a.increaseMyInt()
print(a.myLazyVar) // 1: "initialiser" only called once, OK
a.myLazyVar += 1
print(a.myLazyVar) // 2: however we can still mutate the value
// directly if we so wishes
The short answer is as other have said is "no" but there is a way to get the effect using a private lazy var and computed var.
private lazy var _username: String? = {
return loadUsername()
}()
var username: String? {
set {
// Do willSet stuff in here
if newValue != _username {
saveUsername(newValue)
}
// Don't forget to set the internal variable
_username = newValue
// Do didSet stuff here
// ...
}
get {
return _username
}
}
No
points is a constant, you cannot set anything to it. The only difference to a let constant is that it is (potentially) initialized later.
This answer provides a little more information as to why you use var instead of let in the lazy case.
Edit: to make the answer not look to empty
Take a look at this blog post where the author raises a few valid points regarding why observing lazy vars might not yet be supported. What should oldValue be in the observer? nil? That would not be a good idea in your non-optional case.
Yes, Started by version Swift 5.3 Property observers can now be attached to lazy properties.
class C {
lazy var property: Int = 0 {
willSet { print("willSet called!") } // Okay
didSet { print("didSet called!") } // Okay
}
}
or
class C {
lazy var property: Int = { return 0 }() {
willSet { print("willSet called!") } // Okay
didSet { print("didSet called!") } // Okay
}
}
Please take a look at the links below for more:
https://www.swiftbysundell.com/tips/lazy-property-observers/
https://github.com/apple/swift/blob/main/CHANGELOG.md
This question already has answers here:
Swift: how to change a property's value without calling its didSet function
(4 answers)
Closed 7 years ago.
In Objective-C:
#interface User : NSObject
#property (nonatomic, strong) NSString *name;
- (void)setName:(NString *newName) {
_name = newName
NSLog("newName = %#", newName);
}
User *user = [[User alloc] init];
user.name = #"Test"; // Call setName method and log "newName = Test"
But in an internal method of the User class:
_name = "Test"; // Don't call setName
In Swift:
class User : NSObject
var name: String {
didSet {
print("newName = " + name)
}
}
How to set name in the internal method of the User class without triggering the didSet observer? Like_name = #"Test" in Objective-C?
You cannot prevent the didSet from being called. However, if you want to, for some reason, recreate the mechanics of calling the instance-variable directly in Objective-C, avoiding the setter, you can with a computed/stored property pair.
For example:
class User {
private var _name: String = ""
var name: String {
get {
return _name
}
set {
// any willSet logic
_name = newValue
// any didSet logic
}
}
}
In essence, this is actually approximately exactly what you actually get in Objective-C when you create a property.
From the Swift docs:
A Swift property does not have a corresponding instance variable, and the backing store for a property is not accessed directly. This approach avoids confusion about how the value is accessed in different contexts and simplifies the property’s declaration into a single, definitive statement.
And:
The willSet and didSet observers for totalSteps are called whenever the property is assigned a new value. This is true even if the new value is the same as the current value.
There's no exact analogy to backing variables integrated into Swift. You can replicate Objective-C properties (see nhgrif's answer). That approach would be foreign in Swift, though, and might be difficult for future programmers to understand. If you don't want to do that, you could either move your didSet code to a separate function…
class User : NSObject {
var name: String = ""
func setNameAndPrint(name newName : String) {
name = newName
print("newName = " + name)
}
}
let user = User()
user.name = "Aaron" // doesn't print anything
user.setNameAndPrint(name: "Zhao Wei") // prints "newName = Zhao Wei"
… or you could write more explicit code to mimic this behavior …
class User : NSObject {
var printNameAfterSetting = true
var name: String = "" {
didSet {
if printNameAfterSetting {
print("newName = " + name)
} else {
printNameAfterSetting = true
}
}
}
}
let user = User()
user.printNameAfterSetting = false
user.name = "Aaron" // prints nothing
user.name = "Zhao Wei" //prints "newName = Zhao Wei"
This example uses a bool, but you could use an enum or other type to represent more complex logic depending on your use case.