Skip to content

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

@tanukineiri

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()
    }
}

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