Skip to content

The Key handles tap with delay if the Keyboard is in a ScrollView (on iOS) #33

Description

@tanukineiri

macOS Version(s) Used to Build

macOS 13 Ventura

Xcode Version(s)

Xcode 14

Description

The key handles tap with delay if the Кeyboard is in a ScrollView. See the modified KeyboardDemo example below.

The bug only occurs on iOS.
On macOS, the Keyboard in a ScrollView works correctly.

Crash Logs, Screenshots or Other Attachments (if applicable)

Screenshot from KeyboardDemo

Modified ContentView.swift from KeyboardDemo to demonstrate the bug:

import Keyboard
import SwiftUI
import Tonic

let evenSpacingInitialSpacerRatio: [Letter: CGFloat] = [
    .C: 0.0,
    .D: 2.0 / 12.0,
    .E: 4.0 / 12.0,
    .F: 0.0 / 12.0,
    .G: 1.0 / 12.0,
    .A: 3.0 / 12.0,
    .B: 5.0 / 12.0
]

let evenSpacingSpacerRatio: [Letter: CGFloat] = [
    .C: 7.0 / 12.0,
    .D: 7.0 / 12.0,
    .E: 7.0 / 12.0,
    .F: 7.0 / 12.0,
    .G: 7.0 / 12.0,
    .A: 7.0 / 12.0,
    .B: 7.0 / 12.0
]

let evenSpacingRelativeBlackKeyWidth: CGFloat = 7.0 / 12.0

struct PitchRange: Identifiable {
    let range: ClosedRange<Int>
    let id: Int
    
    var lowerBound: Int { range.lowerBound }
    var upperBound: Int { range.upperBound }
}


struct ContentView: View {
    @State private var currentKeyboardId = 2

    internal let ranges = [36...47, 48...59, 60...71, 72...83, 84...95, 96...107].enumerated().map {
        PitchRange(range: $0.element, id: $0.offset)
    }


    func noteOn(pitch: Pitch, point: CGPoint) {
        print("note on \(pitch)”)
    }

    func noteOff(pitch: Pitch) {
        print("note off \(pitch)”)
    }

    func noteOnWithVerticalVelocity(pitch: Pitch, point: CGPoint) {
        print("note on \(pitch), midiVelocity: \(Int(point.y * 127))”)
    }

    func noteOnWithReversedVerticalVelocity(pitch: Pitch, point: CGPoint) {
        print("note on \(pitch), midiVelocity: \(Int((1.0 - point.y) * 127))”)
    }

    var randomColors: [Color] = (0 ... 12).map { _ in
        Color(red: Double.random(in: 0 ... 1),
              green: Double.random(in: 0 ... 1),
              blue: Double.random(in: 0 ... 1), opacity: 1)
    }

    @State var lowNote = 24
    @State var highNote = 48

    @State var scaleIndex = Scale.allCases.firstIndex(of: .chromatic) ?? 0 {
        didSet {
            if scaleIndex >= Scale.allCases.count { scaleIndex = 0 }
            if scaleIndex < 0 { scaleIndex = Scale.allCases.count - 1 }
            scale = Scale.allCases[scaleIndex]
        }
    }

    @State var scale: Scale = .chromatic
    @State var root: NoteClass = .C
    @State var rootIndex = 0
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        HStack {
            Keyboard(layout: .verticalIsomorphic(pitchRange: Pitch(48) ... Pitch(77))).frame(width: 100)
            VStack {
                HStack {
                    Stepper("Lowest Note: \(Pitch(intValue: lowNote).note(in: .C).description)”,
                            onIncrement: {
                                if lowNote < 126, highNote > lowNote + 12 {
                                    lowNote += 1
                                }
                            },
                            onDecrement: {
                                if lowNote > 0 {
                                    lowNote -= 1
                                }
                            })
                    Stepper("Highest Note: \(Pitch(intValue: highNote).note(in: .C).description)”,
                            onIncrement: {
                                if highNote < 126 {
                                    highNote += 1
                                }
                            },
                            onDecrement: {
                                if highNote > 1, highNote > lowNote + 12 {
                                    highNote -= 1
                                }

                            })
                }
                
/// BUG DEMO BEGIN
                GeometryReader { geoProxy in
                    ScrollViewReader { scrollProxy in
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack(spacing: 0) {
                                ForEach(ranges) { range in
                                    Keyboard(layout: .piano(pitchRange: Pitch(intValue: range.lowerBound) ... Pitch(intValue: range.upperBound)),
                                             noteOn: noteOnWithVerticalVelocity(pitch:point:), noteOff: noteOff)
                                    .frame(minWidth: geoProxy.size.width * 0.5)
                                    .id(range.id)
                                }
                            }
                            .frame(height: 200)
                            .frame(maxWidth: .infinity)
                        }
                        .background(.black)
                        .onChange(of: currentKeyboardId) { newValue in
                            withAnimation {
                                scrollProxy.scrollTo(newValue)
                            }
                        }
                        .onAppear {
                            scrollProxy.scrollTo(currentKeyboardId)
                        }
                    }
                }
/// BUG DEMO END

                HStack {
                    Stepper("Root: \(root.description)”,
                            onIncrement: {
                        let allSharpNotes = (0...11).map { Note(pitch: Pitch(intValue: $0)).noteClass }
                        var index = allSharpNotes.firstIndex(of: root.canonicalNote.noteClass) ?? 0
                        index += 1
                        if index > 11 { index = 0}
                        if index < 0 { index = 1}
                        rootIndex = index
                        root = allSharpNotes[index]
                    },
                            onDecrement: {
                        let allSharpNotes = (0...11).map { Note(pitch: Pitch(intValue: $0)).noteClass }
                        var index = allSharpNotes.firstIndex(of: root.canonicalNote.noteClass) ?? 0
                        index -= 1
                        if index > 11 { index = 0}
                        if index < 0 { index = 1}
                        rootIndex = index
                        root = allSharpNotes[index]
                    })

                    Stepper("Scale: \(scale.description)”,
                            onIncrement: { scaleIndex += 1 },
                            onDecrement: { scaleIndex -= 1 })
                }
                Keyboard(layout: .isomorphic(pitchRange:
                                                Pitch(intValue: 12 + rootIndex) ... Pitch(intValue: 84 + rootIndex),
                                             root: root,
                                             scale: scale),
                         noteOn: noteOnWithReversedVerticalVelocity(pitch:point:), noteOff: noteOff)
                .frame(minWidth: 100, minHeight: 100)

                Keyboard(layout: .guitar(),
                         noteOn: noteOn, noteOff: noteOff) { pitch, isActivated in
                    KeyboardKey(pitch: pitch,
                                isActivated: isActivated,
                                text: pitch.note(in: .F).description,
                                pressedColor: Color(PitchColor.newtonian[Int(pitch.pitchClass)]),
                                alignment: .center)
                }
                .frame(minWidth: 100, minHeight: 100)

                Keyboard(layout: .isomorphic(pitchRange: Pitch(48) ... Pitch(65))) { pitch, isActivated in
                    KeyboardKey(pitch: pitch,
                                isActivated: isActivated,
                                text: pitch.note(in: .F).description,
                                pressedColor: Color(PitchColor.newtonian[Int(pitch.pitchClass)]))
                }
                .frame(minWidth: 100, minHeight: 100)

                Keyboard(latching: true, noteOn: noteOn, noteOff: noteOff) { pitch, isActivated in
                    if isActivated {
                        ZStack {
                            Rectangle().foregroundColor(.black)
                            VStack {
                                Spacer()
                                Text(pitch.note(in: .C).description).font(.largeTitle)
                            }.padding()
                        }

                    } else {
                        Rectangle().foregroundColor(randomColors[Int(pitch.intValue) % 12])
                    }
                }
                .frame(minWidth: 100, minHeight: 100)
            }
            Keyboard(
                layout: .verticalPiano(pitchRange: Pitch(48) ... Pitch(77),
                                       initialSpacerRatio: evenSpacingInitialSpacerRatio,
                                       spacerRatio: evenSpacingSpacerRatio,
                                       relativeBlackKeyWidth: evenSpacingRelativeBlackKeyWidth)
            ).frame(width: 100)
        }
        .background(colorScheme == .dark ?
                    Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions