1

In my app, I give the users the option to press on images to show them full screen. When the image is displayed in full screen users can zoom in on it and should be able to drag it around to see other portions of the image.

Currently when the image is zoomed in and I try to drag it to any side it re-centers and doesn't drag well. Also I can only zoom into the center. How can I modify this code so that the drag gesture to any side when zoomed in is smooth and a zoom gesture can be done on the sides of the image not just the center?

The KFImage is a simple package that renders images from a url. It can be added to Xcode with this url: https://github.com/onevcat/Kingfisher.git.

import SwiftUI
import Kingfisher
import UIKit

struct ContentView: View {
    @State private var myPhoto = "https://firebasestorage.googleapis.com:443/v0/b/hustle-85b6c.appspot.com/o/messages%2F7AC5914A-6239-41CF-85EF-E1C0F25C0A84?alt=media&token=8720789b-7cdd-410c-9532-143a2bcf3f3b"
    @State private var currentScale: CGFloat = 1.0
    @State private var previousScale: CGFloat = 1.0
    @State private var currentOffset = CGSize.zero
    @State private var previousOffset = CGSize.zero
    @State var imageHeight: Double = 0.0
    @State var imageWidth: Double = 0.0
    
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(.black)
            GeometryReader { geometry in
                KFImage(URL(string: myPhoto))
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .scaleEffect(max(self.currentScale, 1.0))
                    .offset(x: self.currentOffset.width + (widthOrHeight(width: true) - imageWidth) / 2.0, y: self.currentOffset.height + (widthOrHeight(width: false) - imageHeight) / 2.0)
                    .background ( /// this background is to center the image (image loaded async so have to wait for it to load and then get height)
                        GeometryReader { proxy in
                            Color.clear
                                .onChange(of: proxy.size.height) { _ in
                                    imageHeight = proxy.size.height
                                    imageWidth = proxy.size.width
                                }
                            
                        }
                    )
                    .gesture(
                        SimultaneousGesture(
                            DragGesture()
                                .onChanged { value in
                                    let deltaX = value.translation.width - self.previousOffset.width
                                    let deltaY = value.translation.height - self.previousOffset.height
                                    previousOffset.width = value.translation.width
                                    previousOffset.height = value.translation.height
                                    let newOffsetWidth = self.currentOffset.width + deltaX / self.currentScale
                                    let newOffsetHeight = self.currentOffset.height + deltaY / self.currentScale
                                    withAnimation(.linear(duration: 0.25)){
                                        if abs(newOffsetWidth) <= geometry.size.width - 200.0 && abs(newOffsetWidth) >= -200.0 {
                                            self.currentOffset.width = newOffsetWidth
                                        }
                                        if abs(newOffsetHeight) < 450 {
                                            self.currentOffset.height = newOffsetHeight
                                        }
                                    }
                                }
                                .onEnded { _ in
                                    withAnimation(.easeInOut){
                                        if currentScale < 1.2 {
                                            self.previousOffset = CGSize.zero
                                            self.currentOffset = CGSize.zero
                                        }
                                    }
                                },
                            MagnificationGesture().onChanged { value in
                                let delta = value / self.previousScale
                                let newScale = self.currentScale * delta
                                withAnimation {
                                    if newScale <= 3.5 {
                                        self.previousScale = value
                                        self.currentScale = newScale
                                    }
                                    if newScale < 1.3 {
                                        self.previousScale = 1.0
                                        self.currentScale = 1.0
                                        self.previousOffset = CGSize.zero
                                        self.currentOffset = CGSize.zero
                                    }
                                }
                            }
                        )
                    )
            }
        }.ignoresSafeArea()
    }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
ahmed
  • 341
  • 2
  • 9

1 Answers1

1

Looking at a similar implementation, you could try to employ a conditional check to maintain the view within boundaries.

Something like:

import SwiftUI
import Kingfisher

struct ContentView: View {
    @State private var myPhoto = "https://firebasestorage.googleapis.com:443/v0/b/hustle-85b6c.appspot.com/o/messages%2F7AC5914A-6239-41CF-85EF-E1C0F25C0A84?alt=media&token=8720789b-7cdd-410c-9532-143a2bcf3f3b"
    @State private var currentScale: CGFloat = 1.0 // Removed previousScale
    @State private var currentOffset = CGSize.zero  // Removed previousOffset
    
    var body: some View {
        ZStack {
            Rectangle().foregroundColor(.black)
            GeometryReader { geometry in // Use dynamic offsets and scaling factors based on GeometryReader
                KFImage(URL(string: myPhoto))
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .scaleEffect(currentScale) // Removed max scaling, use currentScale directly for smoother scaling
                    .offset(x: currentOffset.width, y: currentOffset.height) // Use currentOffset directly for smoother dragging
                    .gesture(
                        SimultaneousGesture(
                            DragGesture()
                                .onChanged { value in
                                    // Calculate dynamic offset while dragging
                                    self.currentOffset.width = value.translation.width / self.currentScale
                                    self.currentOffset.height = value.translation.height / self.currentScale
                                }
                                .onEnded { value in
                                    // Set previousOffset to currentOffset to maintain position
                                },
                            MagnificationGesture()
                                .onChanged { value in
                                    // Calculate dynamic scaling
                                    let newScale = self.currentScale * value
                                    self.currentScale = min(max(newScale, 1.0), 3.0) // Use min-max to restrict scaling
                                }
                                .onEnded { value in
                                    // Reset to default scaling for next gesture
                                    self.currentScale = 1.0
                                }
                        )
                    )
                    .clipped() // Clip the view to its bounding frame
                    .onAppear {
                        // Reset on appear
                        self.currentScale = 1.0
                        self.currentOffset = CGSize.zero
                    }
            }
        }.ignoresSafeArea()
    }
}

But consider also an alternative approach, using a ZoomableContainer presented here.

The OP mentions the alternative solution: "Isn't there an easy way to pinch to zoom in an image in SwiftUI?"

It is the SwiftUIImageViewer from Liubov Ilina.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Hey I tried this code on an actual device and Im encountering the same problem. The image does not drag when zoomed in. It does a weird recenter every time I try to drag to any direction when zoomed in. – ahmed Aug 26 '23 at 20:28
  • Thanks for sharing the link above. I was able to use an answer on it that fixed my issue. here is the specific solution that worked: https://stackoverflow.com/a/75411712/18070009 – ahmed Aug 26 '23 at 20:38
  • @ahmed Good catch. I have edited the answer to reference your solution. – VonC Aug 26 '23 at 20:58
  • If you want an interesting one...you have to work with html and swift: https://stackoverflow.com/questions/76990395/video-reloads-instead-of-pausing-wkwebview-swift – ahmed Aug 28 '23 at 06:38