stone
stone

Reputation: 2202

Can't set nil to an optional property with custom setter in Swift

I have swift class with various properties, some of which have optional types.

class UserObject: PFUser{

   //optional property
   var optionalPhotoURL:String? {
       get {
           if let optionalPhotoURL:String = objectForKey("optionalPhotoURL"){
               return optionalPhotoURL
           }
           //not needed but just for emphasis
           return nil
       }

       //I am unable to set UserObject.optionalPhotoURL = nil with this setters
       set {
          if let newPhotoURL:String = newValue! {
              setObject(newPhotoURL, forKey: "optionalPhotoURL")
           }else{
             self.optionalPhotoURL = nil
           }
       }
   } 
}

I am unable to set optionalPhotoURL as nil, if it already had a previous value assigned to it.

I guess my question is, how do i "unset" an optional property with custom setter?

Update

These setters all crash

   set {
      if let newPhotoURL:String = newValue {
          setObject(newPhotoURL, forKey: "optionalPhotoURL")
       }else{
         self.optionalPhotoURL = nil
       }
   }

and this

    set {
        if (newValue != nil) {
            setObject(newValue!, forKey: "optionalPhotoURL")
        }else{
            self.optionalPhotoURL = nil
        }
    }

Upvotes: 2

Views: 2393

Answers (3)

vadian
vadian

Reputation: 285092

Never set a value of a computed property in its set scope by calling itself !
This causes an infinite loop and the app will crash.

I don't know which API setObject:forKey belongs to, but in the case of nil you are supposed to remove the object

set {
    if let newPhotoURL = newValue {
       setObject(newPhotoURL, forKey: "optionalPhotoURL")
    } else {
       removeObjectForKey("optionalPhotoURL")
    }
 }

Upvotes: 2

nhgrif
nhgrif

Reputation: 62062

What you have here is a computed property.

Swift properties can either be computed or stored. We can observe value changes in our stored properties by using didSet and willSet but here we still have a stored property.

In your case, since you have overridden set and get*, you don't have a stored property, you have a computed property. If you want a computed property to have a backing storage, you must create that independently.

So you may want something like this:

class FooClass {
    private var storageProperty: String?
    var accessProperty: String? {
        get {
            return self.storageProperty
        }
        set {
            self.storageProperty = newValue
            // or whatever logic you may like here
        }
    }
} 

*: You can't override set without also overriding get. You can however override get without overriding set--this makes a readonly computed value.


Moreover, it's important that we implement our storage properties in this way over relying on key-value coding.

For starters, setObject(forKey:) approach doesn't even work on pure Swift types. This will only work on objects which inherit from Objective-C types. It's an inherited method from NSObject's compliance to NSKeyValueCoding protocol. Why the base object of Objective-C conforms to so many protocols is beyond me... but it does and there's nothing we can do about it.

If we have a code base in which some of our objects are inheriting from Objective-C objects (which basically any project will have, UIViewController, etc), and some of our objects are pure Swift objects (which you will tend to have when you're creating your own Swift classes from scratch), then our Swift objects will not be able to implement this same pattern. If we have some objects of both types, we'll either have to implement the pattern I show above for all of them (and then we have consistency) or we'll have to implement one pattern for some types and another for other types (Swift structs would definitely have to implement the above pattern, you can't just make them inherit from NSObject) (and then we have inconsistency, which we don't like).

But what's far worse about setObject(forKey:) is that the first argument of this method always will be of type AnyObject. There is no type safety to the method at all. Where things are stored via setObject(forKey:) is based purely on the key which we use. When we use setObject(forKey:), we take a pile of type-safety advantages that Swift gives us and we throw them out the window. If we don't care to leverage the advantages Swift gives us over Objective-C, why are we writing it in Swift at all?

We can't even make the stored property private when we use setObject(forKey:). Anyone who knows the key can call setObject(forKey:) and set that object to whatever they want. And that includes objects which are not strings. All we have to do to introduce a crash to this codebase is write a class extension or subclass which has a key collision on a different type other than what you've used (so maybe an NSData value for the same key). And even if it doesn't happen to be a different type, simply duplicating the same key for multiple properties is going to introduce bugs... and bugs that are hard to debug.

Upvotes: 4

TPlet
TPlet

Reputation: 948

Your property optionalPhotoURL is a computed property, it does not store any values:
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
You might want to create an additional property which actually stores the value. However, why do you want to set it to nil, since you are not deleting they object in case of nil.

Upvotes: 0

Related Questions