0

I have a UILongPressGestureRecognizer, handleTap assigned to my UIView Keyboard instance. The keyboard has a bunch of CAShapelayer sublayers named Keys. I finally figured out how to get the location of my touch and (hopefully properly) use hitTest on my layers to assign the touch to the correct layer. I can get the correct key to light up and play sound. But how do I allow for multiple keys to register touches simultaneously using this method? Is it possible?

This is the relevant code from my ViewController:

var tapIndex = 0
    var keyboardIndex = 1
    var keyboards = [Keyboard]()
    var masterKeyboard: Keyboard!
    var masterKeys = [Key]()
    var masterHighlightKey = Int()
    var chordCount = 0
    var chordBorders = [CAShapeLayer]()
    var masterChordBorders = [CAShapeLayer]()
    var chordBorderColors: [UIColor] = [darkerYellow, lightPurple, darkerGreen, .orange, darkerBlue]

    let engine = AudioEngine(waveform1: AKTable(.sawtooth), waveform2: AKTable(.square))

    @objc func handleTap(_ recognizer: UILongPressGestureRecognizer) {

        let tapLocation = recognizer.location(in: recognizer.view)
        var key: Key!
        var tag = Int()
        if recognizer.state == .began {
            print(tapLocation)
        }

        if tapIndex != -1 {
            for layer in masterKeyboard.keys {
                if layer.hitTest(tapLocation) != nil {
                    key = layer
                    tag = key.tag
                    break
                }
            }
            if key != nil {
                let digits = tag.digits
                var myTagString = String(tag)
                var myTagCount = tag.digitsCount
                var myKeyboardTag = Int()

                if myTagCount <= 3 {
                    myKeyboardTag = tag.digits[0] - 1
                } else {
                    myKeyboardTag = "\(digits[0])\(digits[1])".int! - 1
                }

                let myKeyboard = self.keyboards[myKeyboardTag]
                let myCount = self.keyboards[myKeyboardTag].keys.count
                let myKeys = self.keyboards[myKeyboardTag].keys

                myTagString.remove(at: myTagString.startIndex)
                var myTag = Int()

                if myKeyboard.tag < 1000 {
                    myTag = myTagString.int!
                    //            print("myTag is \(myTag)")
                } else {
                    var length = Int()
                    if myTagString.count < 5 {
                        length = 1
                    } else {
                        length = 2
                    }
                    myTag = myTagString.slicing(from: 3, length: length)!.int!
                }

                let unison = 0, min2nd = 1, maj2nd = 2, min3rd = 3, maj3rd = 4, P4th = 5, tritone = 6, P5th = 7, min6th = 8, maj6th = 9, min7th = 10, maj7th = 11, octave = 12
                let simpleIntervals = [unison, min2nd, maj2nd, min3rd, maj3rd, P4th, tritone, P5th, min6th, maj6th, min7th, maj7th, octave]

                // -1: notes off, 0: single notes, 1: major triads, 2: minor triads, 3: aug triads, 4: dim triads, 5: sus4 triad, 6: sus2 triad

                let chordInversionOuterBounds = [1: [8, 9, 10], 2: [8, 10, 9], 3: [9, 9, 9], 4: [7, 10, 10], 5: [8, 8, 11], 6: [8, 11, 8]]
                let chordUpperOffsets = [1: [8, 5], 2: [8, 4], 3: [9, 5], 4: [7, 4], 5: [8, 6], 6: [8, 3]]
                let chordInversions = [1: [4, 7, 5, 8], 2: [3, 7, 5, 9], 3: [4, 8, 4, 8], 4: [3, 6, 6, 9], 5: [5, 7, 5, 7], 6: [2, 7, 5, 10]]

                let myRoot = myKeys[myTag]
                let midiRootOffset = myKeyboard.startingPitch + myTag + 21
                let myRootMidiNote = MIDINoteNumber(midiRootOffset)

                var my3rd = Key()
                var my3rdMidiNote = MIDINoteNumber()

                var my5th = Key()
                var my5thMidiNote = MIDINoteNumber()

                func toggleChordShape(triadType: Int, addRemove: Bool) {
                    let rootPosOffset = chordUpperOffsets[triadType]![0]
                    let firstInvOffset = chordUpperOffsets[triadType]![1]
                    let chord = chordInversions[triadType]

                    let myChordInversionOuterBounds = chordInversionOuterBounds[triadType]!.sorted()

                    func set3rdAnd5th() {
                        if myTag <= myCount - rootPosOffset {
                            my3rd = myKeys[myTag + chord![0]]
                            my5th = myKeys[myTag + chord![1]]

                            my3rdMidiNote = MIDINoteNumber(midiRootOffset + chord![0])
                            my5thMidiNote = MIDINoteNumber(midiRootOffset + chord![1])
                        } else if myTag > myCount - rootPosOffset && myTag <= myCount - firstInvOffset {
                            my3rd = myKeys[myTag + chord![0]]
                            my5th = myKeys[myTag - chord![2]]

                            my3rdMidiNote = MIDINoteNumber(midiRootOffset + chord![0])
                            my5thMidiNote = MIDINoteNumber(midiRootOffset - chord![2])
                        } else {
                            my3rd = myKeys[myTag - chord![3]]
                            my5th = myKeys[myTag - chord![2]]

                            my3rdMidiNote = MIDINoteNumber(midiRootOffset - chord![3])
                            my5thMidiNote = MIDINoteNumber(midiRootOffset - chord![2])
                        }
                    }

                    // addRemove == true, highlight; addRemove == false, remove highlights
                    if addRemove {
                        highlightKeys(myKey: myRoot, myRoot: myRoot, highlightColor: keyHighlightColor, doHighlight: true)
                        myRoot.playCount += 1
                        if myCount - myChordInversionOuterBounds[2] > 0 {
                            set3rdAnd5th()
                            my3rd.playCount += 1
                            my5th.playCount += 1
                            highlightKeys(myKey: my3rd, myRoot: myRoot, highlightColor: secondKeyHighlightColor, doHighlight: true)
                            highlightKeys(myKey: my5th, myRoot: myRoot, highlightColor: secondKeyHighlightColor, doHighlight: true)
                        }
                    } else {
                        highlightKeys(myKey: myRoot, myRoot: myRoot, highlightColor: keyHighlightColor, doHighlight: false)
                        myRoot.playCount -= 1
                        if myCount - myChordInversionOuterBounds[2] > 0 {
                            set3rdAnd5th()
                            my3rd.playCount -= 1
                            my5th.playCount -= 1
                            highlightKeys(myKey: my3rd, myRoot: myRoot, highlightColor: secondKeyHighlightColor, doHighlight: false)
                            highlightKeys(myKey: my5th, myRoot: myRoot, highlightColor: secondKeyHighlightColor, doHighlight: false)
                        }
                    }
                }
                if recognizer.state == .began  {
                    if !myRoot.holding {
                        engine.noteOn(note: myRootMidiNote, bank: 1)
                        myRoot.holding = true
                    }
                    if tapIndex == 0 {
                        myRoot.playCount += 1
                        highlightKeys(myKey: myRoot, myRoot: myRoot, highlightColor: keyHighlightColor, doHighlight: true)
                    } else if tapIndex > 0 {
                        toggleChordShape(triadType: tapIndex, addRemove: true)
                        engine.noteOn(note: my3rdMidiNote, bank: 1)
                        engine.noteOn(note: my5thMidiNote, bank: 1)
                    }
                    myRoot.isPlaying = true
                }

                if recognizer.state == .ended {
                    func ifNotHolding(note: Key, midiNote: MIDINoteNumber) {
                        if !note.holding {
                            engine.noteOff(note: midiNote, bank: 1)
                        }
                    }

                    myRoot.holding = false
                    if myRoot.playCount == 1 {
                        engine.noteOff(note: myRootMidiNote, bank: 1)
                    }
                    if tapIndex == 0 {
                        highlightKeys(myKey: myRoot, myRoot: myRoot, highlightColor: keyHighlightColor, doHighlight: false)
                        myRoot.playCount -= 1
                    } else if tapIndex > 0 {
                        toggleChordShape(triadType: tapIndex, addRemove: false)
                        ifNotHolding(note: my3rd, midiNote: my3rdMidiNote)
                        ifNotHolding(note: my5th, midiNote: my5thMidiNote)
                    } else {
                        print("Error!")
                    }
                    myRoot.isPlaying = false
                }

                if recognizer.state == .cancelled {
                    for (index, key) in myKeys.enumerated() {
                        cancelAll(key: key, midiNote: MIDINoteNumber(myKeyboard.startingPitch + index + 21), bank: 1)
                    }
                }
            }
        }
    }

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let screenWidth = view.height
        let screenHeight = view.width

        func addKeyboard(initialKey: Int, startingOctave: Int, numberOfKeys: Int, highlightLockKey: Int) {
            let myKeyboard = Keyboard(initialKey: initialKey, startingOctave: startingOctave, numberOfKeys: numberOfKeys)
            myKeyboard.highlightKey = highlightLockKey
            func tagAppendAndSort() {
                myKeyboard.tag = keyboardIndex
                //        print(myKeyboard.tag)
                if keyboards.count < 8 {
                    keyboardIndex += 1
                } else if keyboards.count == 8 {
                    keyboardIndex += 991
                } else {
                    keyboardIndex += 101
                }
                if highlightLockKey >= 0 {
                    masterKeyboard = myKeyboard
                    masterKeyboard.addKeys(highlightLockKey: highlightLockKey)
                    keyboards.append(masterKeyboard)
                    backgroundView.addSubview(masterKeyboard)
                } else {
                    myKeyboard.addKeys(highlightLockKey: highlightLockKey)
                    keyboards.append(myKeyboard)
                    backgroundView.addSubview(myKeyboard)
                }
            }
            tagAppendAndSort()
        }

        view.addSubview(backgroundView)
        backgroundView.frame = CGRect(x: 0.0, y: 0.0, width: screenWidth, height: screenHeight)
//        backgroundView.backgroundColor = .gray
        view.sendSubview(toBack: backgroundView)

        addKeyboard(initialKey: 4, startingOctave: 2, numberOfKeys: 37, highlightLockKey: 12)

        // bottom keyboard
        masterKeyboard.frame = CGRect(x: 0, y: screenHeight - 91 / masterKeyboard.myKeyboardWidthMod * screenWidth, width: screenWidth, height: 91 / masterKeyboard.myKeyboardWidthMod * screenWidth)
        masterKeyboard.setKeyDimensionsAndSpecs(keys: masterKeyboard.keys, screenWidth: screenWidth)

        masterKeyboard.isUserInteractionEnabled = true
        let longPressGR = UILongPressGestureRecognizer()
        longPressGR.minimumPressDuration = 0
        longPressGR.delegate = masterKeyboard
        longPressGR.addTarget(self, action: #selector(handleTap(_:)))
        masterKeyboard.addGestureRecognizer(longPressGR)
    }

Any help is greatly appreciated!

Jake

Jacob Smolowe
  • 369
  • 2
  • 11
  • 1
    I think this [Answer to a similar question](https://stackoverflow.com/questions/6237693/ios-adding-tapgesture-to-multiple-views/6237745) may help. – Jake Feb 13 '18 at 23:53
  • I'm not sure this is the same thing? Or if it is I don't understand how. CAShapeLayers are not the same as views, correct? And also can't handle gestures? So I have one gesture, assigned to one view, that I'd like to register multiple times if I tap on different sublayers of my view simultaneously. Isn't that a different problem? – Jacob Smolowe Feb 13 '18 at 23:58
  • I've already tagged my Keys using instance properties, and I already am able to detect which one is tapped. I just can't detect multiple keys at once and get them to play at the same time. – Jacob Smolowe Feb 14 '18 at 00:01
  • I should probably say, I already built a working version where all the Keys are UIViews, each one has its own gesture recognizer, and they all use the same handler. It works perfectly, but the whole thing loads very slowly and I was hoping to use CALayers instead of UIViews to speed things up. – Jacob Smolowe Feb 14 '18 at 00:03
  • My mistake. But this is more inline with your question! It's a bit old but you could transpose it to swift I'm sure. https://stackoverflow.com/questions/3469211/is-it-possible-to-use-cocoa-touch-gesture-recognizers-with-layers-calayer-obje – Jake Feb 14 '18 at 00:25
  • Thanks, that is definitely more inline with my question! I think I can get multiple touch locations by overriding touchesBegan, touchesEnded and touchesCanceled on my Keyboard class, but doing that causes my keys to hold indefinitely, so I think I may have other problems and I'm not quite sure how to go about fixing them. Back to brainstorming I guess... – Jacob Smolowe Feb 14 '18 at 01:16
  • do you have `multipleTouchEnabled = true` ? – Jake Feb 14 '18 at 02:04
  • I do. So I think I figured out that my main problem was that all my key manipulations were occurring with UIGestureRecognizers instead of overriding touchesBegan etc. and doing things in there. I can get it to recognize multiple touches now, and even turn keys on and off using touchesBegan, touchesEnded, and touchesCanceled. Where I'm running into trouble is touchesMoved. I can't figure out how to specify that when my touch moves out of the frame of a Key, that Key should turn off, because by the time the touch has moved outside the key, the reference key has changed. Any ideas? – Jacob Smolowe Feb 14 '18 at 05:13
  • Welp, I figured that part out too! I already had an instance Bool variable called `holding` which I was using in my gesture handler to tell when a key was still being played, so I turned it on at touchesBegan and was just able to detect when a touch moved left off the key by checking to see if the key at touchLocation was _not_ holding, and turn the original key off. Yay! – Jacob Smolowe Feb 14 '18 at 05:31
  • OK so now it mostly works; I have a test (inspired by the AudioKit polyphonic keyboard) in touchesMoved to see if my previous touch location was in the same key as my current location, and if not, turn off the previous key. Except... sometimes when I drag around, it'll register both my previous location and the current location as on a new key, so that my comparison criterion never gets filled and the key gets stuck. This happens both when I'm on the original key, and when I drag off. So it's somehow not tracking correctly. Is there any way to account for this contingency? – Jacob Smolowe Feb 14 '18 at 17:01

0 Answers0