I am currently working on a large app where we require the ability to track specific events (click, swipe..) on essentially any custom class derived from UIView
. We have, for example, multiple subclasses of UITableView
which all need to respond to click and/or swipe events in different combinations - occurrences of these events then send properties to an external service. Subclassing UIView
and using this as a parent class of all of our other custom subclasses is not an option.
The kicker is that the data sent when these events occur varies based on the page of our app in which the UI elements are displayed. My first thought was to create a Trackable
protocol. Ideally, I'd like to place all of the boilerplate code for setting up gesture recognisers inside an extension of this protocol, but cannot because the #selector
syntax requires an @objc
annotation which is unavailable in protocol extensions.
Furthermore, when I attempt to instead extend UIView
I no longer have access to a property required by the Trackable
protocol and cannot add a default implementation of it because, as mentioned above, extensions do not support variable declaration. Below is a (very rough) idea of what I'd like to achieve. Is this even possible? Does a better pattern exist? I have also looked at the delegate pattern and it does not solve any of the issues above.
protocol Trackable: class {
var propertiesToSend: [String : [String : String]] { get set }
}
extension UIView: Trackable {
//need alternative way to achieve this, as it is not allowed here
var propertiesToSend = [:]
func subscribe(to event: String, with properties: [String : String]) {
propertiesToSend[event] = properties
startListening(for: event)
}
func unsubscribe(from event: String) {
propertiesToSend.removeValue(forKey: event)
}
private func startListening(for event: String) {
switch (event) {
case "click":
let clickRecogniser = UITapGestureRecognizer(target: self, action: #selector(track(event:)))
addGestureRecognizer(clickRecogniser)
case "drag":
for direction: UISwipeGestureRecognizerDirection in [.left, .right] {
let swipeRecogniser = UISwipeGestureRecognizer(target: self, action: #selector(track(event:)))
swipeRecogniser.direction = direction
addGestureRecognizer(swipeRecogniser)
}
default: return
}
}
@objc
func track(event: UIEvent) {
let eventString: String
switch (event.type) {
case .touches:
eventString = "click"
case .motion:
eventString = "drag"
default: return
}
if let properties = propertiesToSend[eventString] {
sendPropertiesToExternalService("Interaction", properties: properties)
}
}
}
Don't make it more complex than it needs to be. UIView
and its subclasses must derive from NSObject
. Read the documentation on objc_getAssociatedObject
and objc_getAssociatedObject
. No need for protocols or other abstractions.
import ObjectiveC
private var key: Void? = nil // the address of key is a unique id.
extension UIView {
var propertiesToSend: [String: [String: String]] {
get { return objc_getAssociatedObject(self, &key) as? [String: [String: String]] ?? [:] }
set { objc_setAssociatedObject(self, &key, newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}
This can be used as follows.
let button = UIButton()
button.propertiesToSend = ["a": ["b": "c"]]
print(button.propertiesToSend["a"]?["b"] ?? "unknown")