Search code examples
iosswiftuiuikituipangesturerecognizeruipinchgesturerecognizer

UIImageView Pinch Gesture not working when wraped in SwiftUI


I'm trying to use UIImageView in my SwiftUI project. But the pinch gesture is not working. I tried several solutions that should work fine in UIKit.

struct UIImageViewRepresentable: UIViewRepresentable {
    let image: UIImage?

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject {
        var parent: UIImageViewRepresentable

        init(_ parent: UIImageViewRepresentable) {
            self.parent = parent
        }

        @objc func handlePinchGesture(_ sender: UIPinchGestureRecognizer) {
            guard let view = sender.view else { return }

                UIView.animate(withDuration: 0.3) {
                    let scaleResult = sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale)
                    guard let scale = scaleResult, scale.a > 1, scale.d > 1 else { return }
                    sender.view?.transform = scale
                    sender.scale = 1
                }
        }
    }

    func makeUIView(context: Context) -> UIImageView {
        let imageView = UIImageView()
        imageView.image = image
        imageView.isUserInteractionEnabled = true
        imageView.addGestureRecognizer(UIPinchGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handlePinchGesture(_:))))
        return imageView
    }

    func updateUIView(_ uiView: UIImageView, context: Context) {
        uiView.image = image
    }
}
struct ContentView: View {
    var body: some View {
        UIImageViewRepresentable(image: UIImage(named: "default"))
            .frame(width: 300, height: 300)
            .background(Color.gray)
            .padding()
    }
}

Without the animation, the image doesn't scale at all. After adding the animate, the image do scaled but immediately goes back to the initial size. Why does this happen?


Solution

  • It seems like SwiftUI is getting/setting the frame of the image view, which is something one should not do when the view's transform is not the identity transform. From frame,

    If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.

    Changes to this property can be animated. However, if the transform property contains a non-identity transform, the value of the frame property is undefined and should not be modified.

    Of course, SwiftUI doesn't care that the view has been transformed, and sets the frame regardless, probably to update the frame of the view according to SwiftUI's own layout rules.

    Wrapping the UIImageView inside another view solves this problem.

    class Wrapper: UIView {
        let imageView: UIImageView
        
        var image: UIImage? {
            get { imageView.image }
            set { imageView.image = newValue }
        }
        
        override init(frame: CGRect) {
            imageView = UIImageView()
            super.init(frame: frame)
            addSubview(imageView)
            imageView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                imageView.topAnchor.constraint(equalTo: topAnchor),
                imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
                imageView.leftAnchor.constraint(equalTo: leftAnchor),
                imageView.rightAnchor.constraint(equalTo: rightAnchor),
            ])
        }
        
        required init?(coder: NSCoder) {
            fatalError()
        }
    }
    
    struct UIImageViewRepresentable: UIViewRepresentable {
        let image: UIImage?
    
        func makeCoordinator() -> Coordinator {
            Coordinator()
        }
    
        @MainActor
        class Coordinator: NSObject {
    
            @objc func handlePinchGesture(_ sender: UIPinchGestureRecognizer) {
                guard let view = sender.view else { return }
                let scaleResult = sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale)
                guard let scale = scaleResult, scale.a > 1, scale.d > 1 else { return }
                sender.view?.transform = scale
                sender.scale = 1
            }
        }
    
        func makeUIView(context: Context) -> Wrapper {
            let wrapper = Wrapper()
            wrapper.image = image
            wrapper.imageView.isUserInteractionEnabled = true
            wrapper.imageView.addGestureRecognizer(UIPinchGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handlePinchGesture(_:))))
            return wrapper
        }
    
        func updateUIView(_ uiView: Wrapper, context: Context) {
            uiView.image = image
        }
    }