NSCopying uses in Swift
Even in apps written in full Swift, interfacing with Objective-C is still a very big part of iOS development. Many types and semantics from Objective-C are hidden underneath the code we write today, and understanding where they come from can help you make better code decisions and just better understand the platform in general. This time, we'll take a look at what NSCopying
and its companion class NSZone
are and what they can do for Swift apps.
Context: How copying objects worked in Objective-C
Objective-C is notable for its weird syntax and for being completely dynamic, but the superset of C was very clever in the way its problems were solved. I mentioned in previous articles that one of my favorite things in Swift is that many compiler features are tied to actual protocols inside the language, and this is the case for Objective-C as well.
In Objective-C, everything was an object. To support the language's dynamic features, objects provided type information to the run time by subclassing NSObject
. Subclassing it also provided multiple helper methods like equality methods like isEqual
, message routing methods like respondsToSelector
, memory management methods like retain/release
and type helper methods like isKindOfClass
. NSObject
is still used to this day in Swift -- many old iOS delegate protocols require the receiver to be an object as they need to check which methods the object can respond to.
An important aspect of the language was that in Objective-C, every object is a reference type. Because the language is insanely dynamic, there's no way to know if the object you're creating in compile time is actually going to look like that in runtime. Everything can be changed in runtime, and because of that, allocating objects on the stack was simply not possible. Everything has to work through shared pointers and dynamic heap allocations, similar to how classes
behave in Swift.
NSString blogName;
// Compiler error: Interface type cannot be statically allocated
NSMutableString *blogNameBuilder = [[NSMutableString alloc] initWithString:@"Swift"];
NSString* blogName = blogNameBuilder;
[blogNameBuilder appendString:@"Rocks"];
NSLog(@"%@", blogNameBuilder); // SwiftRocks
NSLog(@"%@", blogName); // SwiftRocks
Because working with references is not always a good idea, languages have means to differentiate between reference semantics and value semantics, and the common way of achieving value semantics in Swift is to use a struct
. Because Objective-C doesn't have those for objects, its is very different -- instead of having a clear distinction of reference types versus value types like in Swift, Objective-C allowed reference types to opt-in to value semantics through the copy
property accessor:
@property (copy, nonatomic) NSString* blogName;
---
_blogName = @""; // Initialize the ivar
NSMutableString *blogNameBuilder = [[NSMutableString alloc] initWithString:@"Swift"];
self.blogName = blogNameBuilder;
[blogNameBuilder appendString:@"Rocks"];
NSLog(@"%@", blogNameBuilder); // SwiftRocks
NSLog(@"%@", self.blogName); // Swift
With copy
as one of the property's accessors, attributing a value to the property's generated setter will instead attribute a copy-on-write copy of the original value instead of the regular pointer. copy
in Objective-C works by changing the synthesized setter of the property -- instead of simply assigning the new value to the ivar, the synthesized setter calls a copy()
method on the value:
// Created by the runtime:
- (void)setBlogName:(NSString *)blogName {
_blogName = [blogName copy];
}
Calling copy()
returns a new instance of the type, completely separated from the original one. But where does this method comes from?
NSCopying
The copy()
method comes from NSCopying
-- a protocol built to do exactly what it implies: giving objects the ability to generate copies of themselves. Instead of tying the accessor to an internal compiler feature, The copy
accessor works through NSCopying
so value semantics on properties can also be applied to your own custom types as well. As expected, the copy
accessor in Objective-C only works with types that inherit from NSCopying
, and its usage in setters is simply a syntax sugar to calling the protocol manually.
copy()
is the only method defined by NSCopying
, and implementing it is simply a matter of returning a new instance of your type. Let's use a Box
pattern as an example: In Swift, a common way to bring reference semantics to a value type is to "box" a value inside a class
:
class Box<T> {
let element: T
init(element: T) {
self.element = element
}
}
This allows us to create references to an arbitrary T
type, but what if for some reason we wanted to make a copy of this entire box, including the underneath value?
One way to achieve this would be to simply create a new instance:
func copyBox() -> Box<T> {
let newElement = element
let newBox = Box(element: newElement)
return newBox
}
This approach, however, was a small issue: Although we are indeed creating a new instance of the box, we don't really know if the underneath element
is going to be copied appropriately. If the T
type requires doing additional actions to actually copy its contents, the newElement
property would still be partially referencing the original element
one.
One way to fix this would be to make T
inherit from some "copying" protocol, giving us the ability to be sure that we're handling a new copy of the element. Luckily we don't need to, because that's exactly what NSCopying
is for!
class Box<T: NSCopying>: NSCopying {
let element: T
init(element: T) {
self.element = element
}
func copy(with zone: NSZone? = nil) -> Any {
guard let newElement = element.copy() as? T else {
fatalError("Couldn't copy element")
}
let newBox = Box(element: newElement)
return newBox
}
}
By calling copy()
on our box which internally calls copy()
on the element as well, we're now sure that we're handling a reference to a unique copy.
var nsString = NSMutableString(string: "Swift")
let stringBox: Box<NSString> = Box(element: nsString)
let stringBoxCopy = stringBox.copy() as! Box<NSString>
// Getting memory addresses
print(Unmanaged.passUnretained(stringBox).toOpaque()) // 0x0000600003620da0
print(Unmanaged.passUnretained(stringBoxCopy).toOpaque()) // 0x0000600003620e20
nsString.append("Rocks")
// Checking if the inner string was copied as well
print(stringBox.element) // SwiftRocks
print(stringBoxCopy.element) // Swift
(I used NSString
in this case because it inherits from NSCopying
and is easy to use for tests like this.)
NSCopying
can be used in Swift as a generic way to create copies of classes (reference types), and as a bonus, making a Swift type inherit from NSCopying
will also allow it to make use of the copy
property accessor in Objective-C bridges.
@property (copy, nonatomic) MyBridgedNSCopyingSwiftClass* foo;
What the hell is the NSZone
argument?
You might have noticed that copy()
takes a NSZone
argument that is ignored -- what is that and why is it ignored?
If you're looking for a short answer, you'll be happy to know that NSZone
is deprecated and you can completely ignore it. For a bit of iOS history, keep on reading this section.
A common problem with heap allocation algorithms is memory fragmentation. We need to scan the heap to find a place to allocate an object, but if we're not careful about where we place it, deallocating objects can end up dividing our available memory into very small pieces of memory that are unusable by larger objects.
To visualize this, let's assume that we have a 16 bits block of free memory:
| |
Now, let's allocate a 4 bit object called 1, an 8 bit object called 2, and another 4 bit object called 3 to fill our memory:
|1111222222223333|
Now, let's deallocate objects 1 and 3, but not 2:
| 22222222 |
We now have 8 bits of free memory, but if you actually try to allocate an object that has 8 bits of size at this point, you'll not be able to! Even though we do have this space available, this space is fragmented into two blocks of 4 bits that can't be used by larger objects unless we deallocate or move object 2.
The solution to mitigate this at the time was to create zones of memory. Instead of putting everything into one big memory space, an algorithm that allocated/deallocated a lot of memory could create a separate allocation zone to prevent the rest of the physical memory from being fragmented. In this case, the algorithm that creates the large 8 bit object could create a separate 8 bit zone:
Main: |11113333| Zone: |22222222|
After deallocating objects 1 and 3, we'll now have the 8 bits of memory unfragmented and ready to be used by an object of equal size.
Main: | | Zone: |22222222|
This is exactly what NSZone
does. Foundation provided a default object pointing to the "main" zone, but custom ones would be created with custom sizes and granularities.
NSZone* defaultZone = NSDefaultMallocZone();
NSZone* customZone = NSCreateZone(8, 0, YES);
However, as mentioned before, the usage of NSZone
was deprecated long ago. The Objective-C runtimes ignores them completely, and Swift doesn't even let you reference them:
let zone = NSDefaultMallocZone()
// 'NSDefaultMallocZone' is unavailable in Swift: Zone-based memory management is unavailable
The reason is that the creation of ARC rendered it unnecessary as its memory management algorithms were smart enough to prevent fragmentation. Also, fragmentation wasn't much of an issue in iOS anyway -- allocating in virtual memory is much more flexible than in physical memory, and iPhones have enough physical memory to make this a rare occurrence.
NSMutableCopying
For completion purposes, it's important to also mention that Foundation also has a NSMutableCopying
protocol that defines a similar mutableCopy()
method. The functionality is the exact same as NSCopying
-- the difference comes from the fact that it was common in Objective-C to differentiate between immutable and mutable objects. For example, as strings in Objective-C could be defined both as the immutable NSString
and the mutable NSMutableString
, the NSMutableCopying
allowed developers to explicitly create mutable copies of objects. Since let
and var
accessors made this pattern unnecessary in Swift, NSMutableCopying
doesn't have good uses in Swift unless you explicitly create separate mutable versions of your types.
Conclusion
Many features of iOS rely on legacy Objective-C components. As always, knowing the history of why things work the way they do can allow you to make better choices on the platform. In this case, NSCopying
is an important aspect of Objective-C and can still be used not only for Swift-specific uses but also to unlock copy semantics in properties bridged to Objective-C.
If you want to see more Swift and iOS information like this, follow me on my Twitter (@rockbruno_), and let me know of any feedback, suggestions and corrections you want to share.