I came across the NSCopying attribute when working with URLSession and URLSessionConfiguration.

At first glance, I would expect any value that is prefixed with the @NSCopying attribute to return a new copy and any changes would not affect the initial value. Although that does work in theory, there is a scenario where this can be a little confusing.

To start with, let me explain how @NSCopying works, with URLSessionConfiguration in URLSession.

@available(iOS 7.0, *)
open class URLSession : NSObject {

…
    @NSCopying open var configuration: URLSessionConfiguration { get }

…
}

URLSession has a variable called config, which is of type URLSessionConfiguration. Unlike all other variables within URLSession, configuration is prefixed with @NCopying. This stops you from changing a configuration which is already tied to a session. For example

// Retrieving the configuration from an old session but because configuration is prefixed with @NSCopying we are returned a copy of the original configuration.

let config = session.configurataion

config.httpAdditionalHeaders = ["header": "new"]

// If we then change the value of configuration, we can't just call a function on the session and expect it to use the new configuration settings.
 
session.dataTask(with: )

print(session.configurataion.config.httpAdditionalHeaders) // Prints nil

// We would then have to initialise a new session, and pass the new copy of configuration in.

session = URLSession(configuration: config)

print(session.configurataion.config.httpAdditionalHeaders) // Prints ["header": "new"]

So far this is exactly what we would expect to happen. A new copy of the configuration is returned when we retrieve the value.

I thought I would then start to use this for my classes so I started to write the code.

// A simple person class which has a variable called attributes which is prefixed with @NSCopying.

class Person {
  
  @NSCopying var attributes: Attributes

  init(attributes: Attributes) {
    self.attributes = attributes
  }
}

// The attributes class has to first conform to NSCopying otherwise we will get a type conformability error on our attributes variable inside the person class.

class Attributes: NSCopying {
  
  func copy(with zone: NSZone? = nil) -> Any {
   let attribute = Attributes()
    attribute.height = height
    attribute.weight = weight
    return attribute
  }
  
  var height: CGFloat = 180.0
  var weight: CGFloat = 80.0
}

So if we apply the same logic that we did with URLSessionConfiguration, you will see where the confusion lies.

let person = Person(attributes: Attributes())
print(person.attributes.height) // Prints 180  

// We would expect this to return a new copy of attributes right? Wrong!
let attribute = person.attributes
attribute.height = 30

print(person.attributes.height) //Prints 30 

Well, that is confusing. In the URLSessionConfiguration example the object was copied, however when creating a custom class which has a variable prefixed with @NSCopying the value is not copied, and we just returned the original. I found this very confusing, but after some research online and trying to figure out why, I finally found where the issue lies.

When you assign a value in Swift through an initialiser, it sets the value directly and does not go through the setter. Unlike Objective C, where you have the option to go through the setter when initialising. This means that when we set the value, it never goes through the setter, and the value is never copied.

There is currently a swift proposal open with a proposed fix so I recommenced taking a look.

Proposal 153 – Compensate for the inconsistency of @NSCopying‘s behaviour