Search code examples
iosuikit

In a UIScrollView with a large number of tappable image views, what's a good way to improve performance?


There are 100+ image views, and each needs to be tappable. The fact that each has a tap gesture recognizer attached to it has hurt performance. What would be a good strategy for reducing the number of tap gestures, or improving performance?

Rewriting it as a collection view would be too high risk at the moment.


Solution

  • It feels strange that multiple tap gestures would cause you harm but you can get away with a simple single gesture recogniser doing something like the following:

    class ContainerView: UIView {
        
        @IBOutlet private var scrollView: UIScrollView?
        @IBOutlet private var imageViews: [UIImageView]?
        
        private func attachTapGestureRecogniser() {
            addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onImageTapped)))
        }
        
        @objc private func onImageTapped(_ sender: UIGestureRecognizer) {
            guard let tappedImageIndex = imageViews?.firstIndex(where: { $0.bounds.contains(sender.location(in: $0)) }) else {
                // No image was tapped
                return
            }
            print("Image at index \(tappedImageIndex) was tapped")
        }
        
    }
    

    If images are overlapping and firstIndex is not appropriate then you can still get all the image views within the hit area and process them better.

    On the other hand if you have more information you could compute the index possibly like so:

    @objc private func onImageTapped2(_ sender: UIGestureRecognizer) {
        let rowHeight = 200 // Depends on your grid layout
        let columnWidth = 200 // Depends on your grid layout
        let itemsPerRow = 3
        
        let positionInScrollView = sender.location(in: scrollView)
        
        let row = Int(positionInScrollView.y/CGFloat(rowHeight))
        let column = Int(positionInScrollView.x/CGFloat(columnWidth))
        
        let tappedImageIndex = row*itemsPerRow + column
        
        guard tappedImageIndex < imageViews?.count ?? 0 else {
            // No image was tapped
            return
        }
        print("Image at index \(tappedImageIndex) was tapped")
    }
    

    If required you can go into deeper levels using tools like the following:

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let pressLocation = touches.first?.location(in: self) else { return }
        let locationInScrollView = self.convert(pressLocation, to: scrollView)
        // Do stuff here with
    }