@alanzeino Throw dynamic or @objc in front of the method. Adding target/selector is a dynamic function. Learned it the hard way. ¯_(ツ)_/¯
— Joe Fabisevich ™ (@mergesort) May 2, 2015
This Twitter exchange encouraged me to write a few words on what effects the Swift access modifiers have.
Let's consider this example and that we want to use it in a target/action context:
import Foundation
class Thing : NSObject {
func tap() {
println("tap!")
}
}
Using an internal
function or even a public
one will simply just work, but if we change the visibility to private
it will no longer work. The first naive theory would be that this is tied to symbol mangling being different for private
methods.
Little sidebar on what symbol mangling is: it is the mechanism which takes a Swift construct and transforms it to a string that is valid as a symbol in the resulting binary and also unique. If you want some more information on this, read Mike Ash's excellent article on the topic.
So, let's check out the difference in symbol mangling between different access modifiers. First the internal
one:
$ xcrun swiftc test.swift
$ nm test|grep tap
0000000100001050 t __TFC4test5Thing3tapfS0_FT_T_
[...]
$ xcrun swift-demangle __TFC4test5Thing3tapfS0_FT_T_
_TFC4test5Thing3tapfS0_FT_T_ ---> test.Thing.tap (test.Thing)() -> ()
Now the private
variant:
$ nm test|grep tap
00000001000010a0 t __TFC4test5ThingP33_83378C430F65473055F1BD53F3ADCDB73tapfS0_FT_T_
OK, this must be it - the selector mechanism knows nothing about that extra UUID-looking stuff in the private
symbol. Turns out
that adding the @objc
attribute (or @IBAction
for that matter) will make the private
function work as an action, though,
while not affecting the way symbols are mangled, so
this cannot be the reason for the difference in behaviour. It's still interesting to know and actually makes it much harder to
somehow manually call a private
Swift function even if we know that it exists, because only its own compilation unit knows
about the exact value of that extra UUID.
Next route of investigation is using the trusty old Objective-C runtime APIs, considering that target/action is an ObjC mechanism. With the code from this snippet, we can list all methods on our class which are known to the runtime:
import ObjectiveC.runtime
var methodCount : UInt32 = 0
var methods : UnsafeMutablePointer<Method> = class_copyMethodList(self.dynamicType, &methodCount)
for i in 0..<methodCount {
let name = NSStringFromSelector(method_getName(methods[Int(i)]))
println(name)
}
The result is that private
methods do not show up there at all, so they are invisible to the ObjC runtime. This is nice, because
it eliminates another way to bypass the access modifiers at runtime. I guess this excessive hiding is also the reason for private
constructs not being testable at the moment.
To conclude, common Objective-C mechanisms will not work on private
functions, because they are hidden from the runtime. In case
you want to limit the privateness to Swift only, you can utilize the @objc
, @IBAction
or @dynamic
attributes.
UPDATE: Turns out the reason why public/internal methods are not hidden from the ObjC runtime is that @objc
gets implicitly added for those in Objective-C compatible Swift classes.
The compiler does not automatically insert the @objc attribute for declarations marked with the private access-level modifier.
(source)