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?
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
}
}