I had a trick to help test UIAlertController
that worked in Swift 2.x:
extension UIAlertController {
typealias AlertHandler = @convention(block) (UIAlertAction) -> Void
func tapButtonAtIndex(index: Int) {
let block = actions[index].valueForKey("handler")
let handler = unsafeBitCast(block, AlertHandler.self)
handler(actions[index])
}
}
This fails under Swift 3.x with fatal error: can't unsafeBitCast between types of different sizes
, which tempts me to believe there might be a way to make the cast work. Can anyone figure it out?
Found a solution that works in Swift 3.0.1
extension UIAlertController {
typealias AlertHandler = @convention(block) (UIAlertAction) -> Void
func tapButton(atIndex index: Int) {
if let block = actions[index].value(forKey: "handler") {
let blockPtr = UnsafeRawPointer(Unmanaged<AnyObject>.passUnretained(block as AnyObject).toOpaque())
let handler = unsafeBitCast(blockPtr, to: AlertHandler.self)
handler(actions[index])
}
}
}
(Originally, the block
value was the actual block, not a pointer to the block—which you obviously can't cast to a pointer to AlertHandler
)
My answer is based on @Robert Atkins's, but shorter.
The problem here is that, valueForKey
returns a Any
typed object, and because in Swift,
MemoryLayout<Any>.size == 32
MemoryLayout<AnyObjcBlockType>.size == 8
an assertion will be triggered in unsafeBitCast
when casting between types of different sizes.
One work-around is to create an intermediate wrapper and transform back to raw pointer, which satisfies MemoryLayout<UnsafeRawPointer>.size == 8
.
A much simpler way is to create an indirect reference directly using protocol AnyObject
, relying on the fact that MemoryLayout<AnyObject >.size == 8
, we can write following valid code:
typealias AlertHandler = @convention(block) (UIAlertAction) -> Void
func tapButton(atIndex index: Int) {
if let block = actions[index].value(forKey: "handler") {
let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)
handler(actions[index])
}
}
If your UIAlertController is an action sheet you can modify Robert's answer to dismiss the UIAlertController before you executed the handler.
dismiss(animated: true, completion: {() in handler(self.actions[index])})
I was using this extension for testing and without this modification my assertions for presented view controller were failing.