Just had this problem and although I see this question is old, I guess someone might find it as well...
My solution was to use touchesBegan, touchesMoved, touchesEnded and touchesCancelled.
I disabled my buttons although you can turn them into UIViews I guess... then in touchesBegan I use .point(inside:) on all touch events and check on all buttons, calling the buttonDown function on the ones that are touched.
Then on touchesMoved I check again but using both the current touch position and the previous one to see if the touch entered a new button. Basically if a button is touched by the old one and not the new one I call the buttonUp funcion for that button. If it's the other way around I call the buttonDown funcion
Last, on the touchesEnded and touchesCancelled I just see what button is being touched and call the buttonUp function.
I've tested it with multitouch on a real device and in Mac Catalyst and both seem to work fine. It was a quick solution that didn't make it necessary to change anything else from the regular "buttons with target/actions" solution I had before...
Example:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
for b in keys {
if b.point(inside: touch.location(in: b),with:nil) {
keyDown(b)
}
}
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
for b in keys {
if b.point(inside: touch.location(in: b),with:nil) && !b.point(inside: touch.previousLocation(in: b), with:nil) {
keyDown(b)
} else if b.point(inside: touch.previousLocation(in: b), with:nil) && !b.point(inside: touch.location(in: b),with:nil) {
keyUp(b)
}
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
for b in keys {
if b.point(inside: touch.location(in: b),with:nil) {
keyUp(b)
}
}
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
for b in keys {
if b.point(inside: touch.location(in: b),with:nil) {
keyUp(b)
}
}
}
}