Search code examples
swiftswiftuigesture

How can I set this code up to zoom in from anywhere on the image?


I'm working on some code in which users should be allowed to use various gestures on the app's image. What I'd like is to understand how I can allow users to pinch, or double tap to a specific location in the image. Right now, they can only zoom from the center, or else I'm getting unexpected results with the zoom. I chose this path since this uses both SwiftUI, as well as a drag to dismiss feature. Thanks!

import SwiftUI

public struct SampleZoom {
    @ObservedObject
    private var viewModel: ViewModel

    @State private var currentZoom: CGFloat = 0
    @State private var endingZoom: CGFloat = 1

    @State private var isZoomed = false
    @State private var pointTapped: CGPoint = .zero

    @GestureState var swipeOffset: CGSize = .zero

    public init(viewModel: ViewModel) {
        self.viewModel = viewModel
    }
}

extension SampleZoom: View {
    public var body: some View {
        if viewModel.isVisible {
            ZStack {
                Color.black
                    .opacity(viewModel.bgOpacity)
                    .edgesIgnoringSafeArea(.all)
                image(imageData: viewModel.image)
            }
            .onAppear {
                endingZoom = 1
                isZoomed = false
            }
            .overlay(
                /// Close Button
                Button(action: {
                    viewModel.isVisible.toggle()
                }, label: {
                    Image(systemName: "xmark")
                        .foregroundColor(.white)
                        .padding()
                        .background(Color.white.opacity(0.33))
                        .clipShape(Circle())
                })
                    .padding(Spacing.standard), alignment: .topLeading
            )
        } else {
            Spacer()
        }
    }
}

private extension SampleZoom {
    /// Creates card Image
    func image(imageData: Data) -> some View {
        GeometryReader { reader in
            Image("Your Image Here!")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .offset(y: viewModel.imageOffset.height)
                .animation(.default)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .scaleEffect(endingZoom + currentZoom > 1 ? endingZoom + currentZoom : 1, anchor: UnitPoint(
                    x: pointTapped.x / reader.frame(in: .global).maxX,
                    y: pointTapped.y / reader.frame(in: .global).maxY
                ))
                /// Double tap to zoom
                .gesture(
                    TapGesture(count: 2).onEnded {
                        withAnimation {
                            isZoomed.toggle()
                            endingZoom = endingZoom > 1 ? 1 : 2
                        }
                    }
                    .simultaneously(
                        with: DragGesture(minimumDistance: 0, coordinateSpace: .global).onChanged { value in
                            if !isZoomed {
                                pointTapped = value.startLocation
                            }
                        }
                        /// Swipe & close interactions
                        .updating($swipeOffset) { value, outValue, _ in
                            outValue = value.translation
                            viewModel.onChange(imagePosition: swipeOffset)
                        }
                        .onEnded(viewModel.onEnd(swipeDistance:))
                    )
                )
                /// Pinch & zoom interactions
                .gesture(
                    MagnificationGesture()
                        .onChanged { amount in
                            currentZoom = amount - 1
                        }
                        .onEnded { _ in
                            endingZoom += currentZoom
                            currentZoom = 0
                            isZoomed = endingZoom + currentZoom > 1 ? true : false
                        }
                )
        }
    }
}

public extension SampleZoom {
    final class ViewModel: ObservableObject {
        let image: Data

        @Published
        var isVisible: Bool

        @Published
        var imageOffset: CGSize = .zero

        @Published
        var bgOpacity: Double = 1

        func onChange(imagePosition: CGSize) {
            imageOffset = imagePosition

            let halfScreenHeight = UIScreen.main.bounds.height / 2
            let progress = imagePosition.height / halfScreenHeight

            withAnimation(.default) {
                bgOpacity = Double(1 - (progress < 0 ? -progress : progress))
            }
        }

        func onEnd(swipeDistance: DragGesture.Value) {
            withAnimation(.easeOut) {
                var translation = swipeDistance.translation.height

                if translation < Spacing.none {
                    translation = -translation
                }

                if translation < Spacing.doubleExtraLarge * 3 {
                    imageOffset.height = Spacing.none
                    bgOpacity = 1
                } else {
                    isVisible.toggle()
                    imageOffset.height = Spacing.none
                    bgOpacity = 1
                }
            }
        }

        init(image: Data, isVisible: Bool) {
            self.image = image
            self.isVisible = isVisible
        }
    }
}

Solution

  • You can pinch to zoom to your imageView very easily with the help of a UIScrollView. Add UIScrollView and inside add the imageView. Then implement the UIScrollViewDelegate and the function viewForZooming

    override func viewDidLoad() {
        super.viewDidLoad()
    
        scrollView.minimumZoomScale = 1.0
        scrollView.maximumZoomScale = 10.0        
        scrollView.delegate = self
    }  
    
    extension YourVC: UIScrollViewDelegate {
        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return image
        }
    }