I recently ran into a curious problem on one of my side projects that I didn’t understand at first. I was using a swift protocol to provide a uniform interface for various possible kinds of collection view cells that all show a common source of data in different ways, and have a single delegate that they communicate through to respond to user input. A distilled version of what I was doing looked something like this:
It may not be guaranteed that all cells served by the collection view will conform to this protocol, so using optional binding with if-let is the natural way to handle this case for when the cells are the type I expect.
(As a side note, it’s frustrating that this method on UICollectionView returns
UICollectionViewCell?. I use
as!here as I cannot fathom a time when this method doesn’t return a collection view cell; if there is some situation please let me know!)
When I initially wrote this code, I was confused to see a warning show up on the first line inside the if-let statement:
Cannot assign to ‘mydelegate’ in ‘mycell’
Why not? It didn’t make sense to me. I figured that since the
MyCellType protocol was applying to an
@objc type (the
UICollectionViewCell) that the protocol itself also needed to be
This solved my problem, but yielded another. The
MyCellDelegate protocol was not an
@objc protocol, so the compiler complained that I needed to change that protocol as well. It’s certainly reasonable that
@objc protocols can’t reference non
@objc protocols inside of them. I wasn’t terribly happy with this though since none of the code that was using these protocols was written in Objective-C (even though it’s a mixed-language app). If I wanted to use tuples, structs, or generics in these protocols later on then I’d be out of luck.
After playing with this a bit in a playground I was luckily able to stumble onto the answer. The bare essence of the problem can be expressed with the following code:
Setting the name on
thing1 works fine, but setting it on
thing2 gives us the same compiler issue unless the protocol is
@objc. Note that we’re not subclassing any
@objc classes so that’s clearly not the issue here.
I tried changing
MyThing to be a struct to see if the behavior was different, and the following happened:
Which makes sense, since structs assigned to constants are fully immutable: the reference to the struct and the members of the struct are all unable to be changed. Changing the
var for each fixed the errors.
This led me to the eureka moment. The Swift type system cannot reasonably know that the value in a constant whose type is a plain protocol is in fact a class type. If
MyThing really was meant to be a struct, then modifying those fields in the constant definitely should not be allowed. If I’d had any functions on the
MyType protocol that were tagged as
mutating, then trying to call those on
thing2 would have yielded a similar error. If I change
MyThing back to a class and leave
thing2 as a
var, everything works great.
var works for
MyThing, we could also change the original collection view cell example to use if-var instead of if-let to make things work:
Doing this lets us remove the
@objc attributes from
However, I prefer to use
let wherever possible and use
var only when necessary, and in this case, there’s no semantic need for those values to be
var since their references don’t change (though their contents do). The reason
@objc fixed this and allowed my code to use
let is not because the class was also an Objective-C class, but because that attribute also constrains protocols to only apply to class types (which is a swift feature I had forgotten about). We can change the original
MyCellType protocol’s definition to only allow it to apply to class types by doing the following:
This will also have the desired effect, without restricting the kinds of swift features we can use within this protocol. The type system can know that editing a property of a constant of this type is allowed since it can’t be applied to value types, and therefore we don’t need to store a
MyCellType in a var to edit its contents.
It’s certainly a good thing that the compiler yelled at me for doing this the wrong way originally, as if it hadn’t complained and I had used
MyCellType with a struct later on, I could have violated swift’s fundamental invariant for value types stored in a constant. However, I know I would not have done this, and telling the compiler about my intentions with a class protocol makes both me and the compiler happy.
Putting the proper thought into designing your protocols is crucial to getting your code to behave the way you want, and that includes considering if your protocol is meant to behave in a class-like way or in a much broader way. With this distinction, the language can know more about your intentions with how you choose to express your ideas in code, and it can provide benefits like class-based property access along the way.