From 565d80910d2b95776dcb30a4ba03fd812e32e405 Mon Sep 17 00:00:00 2001 From: breadone Date: Tue, 9 Jul 2024 18:49:07 +1200 Subject: [PATCH] Added zoom gesture to expanded view --- JustScanIt.xcodeproj/project.pbxproj | 4 + .../Controllers/ZoomableScrollView.swift | 162 ++++++++++++++++++ JustScanIt/View/MainView.swift | 21 ++- JustScanIt/View/ScanItemView.swift | 1 + 4 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 JustScanIt/Controllers/ZoomableScrollView.swift diff --git a/JustScanIt.xcodeproj/project.pbxproj b/JustScanIt.xcodeproj/project.pbxproj index a062211..8be2a98 100644 --- a/JustScanIt.xcodeproj/project.pbxproj +++ b/JustScanIt.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 46410F842C3D12B100FFBF7E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46410F832C3D12B100FFBF7E /* ZoomableScrollView.swift */; }; 466989C72C34DC7A009884D1 /* JustScanItApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466989C62C34DC7A009884D1 /* JustScanItApp.swift */; }; 466989CB2C34DC7B009884D1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 466989CA2C34DC7B009884D1 /* Assets.xcassets */; }; 466989CE2C34DC7B009884D1 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 466989CD2C34DC7B009884D1 /* Preview Assets.xcassets */; }; @@ -19,6 +20,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 46410F832C3D12B100FFBF7E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = ""; }; 466989C32C34DC7A009884D1 /* JustScanIt.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JustScanIt.app; sourceTree = BUILT_PRODUCTS_DIR; }; 466989C62C34DC7A009884D1 /* JustScanItApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustScanItApp.swift; sourceTree = ""; }; 466989C82C34DC7A009884D1 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -83,6 +85,7 @@ isa = PBXGroup; children = ( 466989D52C34DCE4009884D1 /* VNDocumentCameraViewControllerR.swift */, + 46410F832C3D12B100FFBF7E /* ZoomableScrollView.swift */, ); path = Controllers; sourceTree = ""; @@ -182,6 +185,7 @@ 466989D92C35206E009884D1 /* Scan.swift in Sources */, 466989DF2C353FEC009884D1 /* SettingsView.swift in Sources */, 466989D62C34DCE4009884D1 /* VNDocumentCameraViewControllerR.swift in Sources */, + 46410F842C3D12B100FFBF7E /* ZoomableScrollView.swift in Sources */, 466989DD2C35299D009884D1 /* ScanItemView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/JustScanIt/Controllers/ZoomableScrollView.swift b/JustScanIt/Controllers/ZoomableScrollView.swift new file mode 100644 index 0000000..981bfd3 --- /dev/null +++ b/JustScanIt/Controllers/ZoomableScrollView.swift @@ -0,0 +1,162 @@ +// +// ZoomableScrollView.swift +// JustScanIt +// +// Created by Pradyun Setti on 09/07/2024. +// + +import Foundation +import UIKit +import SwiftUI + +class PinchZoomView: UIView { + + weak var delegate: PinchZoomViewDelgate? + + private(set) var scale: CGFloat = 0 { + didSet { + delegate?.pinchZoomView(self, didChangeScale: scale) + } + } + + private(set) var anchor: UnitPoint = .center { + didSet { + delegate?.pinchZoomView(self, didChangeAnchor: anchor) + } + } + + private(set) var offset: CGSize = .zero { + didSet { + delegate?.pinchZoomView(self, didChangeOffset: offset) + } + } + + private(set) var isPinching: Bool = false { + didSet { + delegate?.pinchZoomView(self, didChangePinching: isPinching) + } + } + + private var startLocation: CGPoint = .zero + private var location: CGPoint = .zero + private var numberOfTouches: Int = 0 + + init() { + super.init(frame: .zero) + + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(pinch(gesture:))) + pinchGesture.cancelsTouchesInView = false + addGestureRecognizer(pinchGesture) + } + + required init?(coder: NSCoder) { + fatalError() + } + + @objc private func pinch(gesture: UIPinchGestureRecognizer) { + + switch gesture.state { + case .began: + isPinching = true + startLocation = gesture.location(in: self) + anchor = UnitPoint(x: startLocation.x / bounds.width, y: startLocation.y / bounds.height) + numberOfTouches = gesture.numberOfTouches + + case .changed: + if gesture.numberOfTouches != numberOfTouches { + // If the number of fingers being used changes, the start location needs to be adjusted to avoid jumping. + let newLocation = gesture.location(in: self) + let jumpDifference = CGSize(width: newLocation.x - location.x, height: newLocation.y - location.y) + startLocation = CGPoint(x: startLocation.x + jumpDifference.width, y: startLocation.y + jumpDifference.height) + + numberOfTouches = gesture.numberOfTouches + } + + scale = gesture.scale + + location = gesture.location(in: self) + offset = CGSize(width: location.x - startLocation.x, height: location.y - startLocation.y) + + case .ended, .cancelled, .failed: + isPinching = false + scale = 1.0 + anchor = .center + offset = .zero + default: + break + } + } + +} + +protocol PinchZoomViewDelgate: AnyObject { + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) +} + +struct PinchZoom: UIViewRepresentable { + + @Binding var scale: CGFloat + @Binding var anchor: UnitPoint + @Binding var offset: CGSize + @Binding var isPinching: Bool + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> PinchZoomView { + let pinchZoomView = PinchZoomView() + pinchZoomView.delegate = context.coordinator + return pinchZoomView + } + + func updateUIView(_ pageControl: PinchZoomView, context: Context) { } + + class Coordinator: NSObject, PinchZoomViewDelgate { + var pinchZoom: PinchZoom + + init(_ pinchZoom: PinchZoom) { + self.pinchZoom = pinchZoom + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangePinching isPinching: Bool) { + pinchZoom.isPinching = isPinching + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeScale scale: CGFloat) { + pinchZoom.scale = scale + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeAnchor anchor: UnitPoint) { + pinchZoom.anchor = anchor + } + + func pinchZoomView(_ pinchZoomView: PinchZoomView, didChangeOffset offset: CGSize) { + pinchZoom.offset = offset + } + } +} + +struct PinchToZoom: ViewModifier { + @State var scale: CGFloat = 1.0 + @State var anchor: UnitPoint = .center + @State var offset: CGSize = .zero + @State var isPinching: Bool = false + + func body(content: Content) -> some View { + content + .scaleEffect(scale, anchor: anchor) + .offset(offset) + .animation(.spring, value: isPinching) + .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) + } +} + +extension View { + func pinchToZoom() -> some View { + self.modifier(PinchToZoom()) + } +} diff --git a/JustScanIt/View/MainView.swift b/JustScanIt/View/MainView.swift index b706d44..1ea7206 100644 --- a/JustScanIt/View/MainView.swift +++ b/JustScanIt/View/MainView.swift @@ -41,6 +41,7 @@ struct MainView: View { var body: some View { NavigationView { ScrollView { +// newScanButton ForEach(scans, id: \.self) { scan in ScanItemView(scan) } @@ -72,6 +73,18 @@ struct MainView: View { } } + var newScanButton: some View { + Button { showVNDocumentCameraView() } label: { + Text("New Scan") + .frame(maxWidth: .infinity) + .padding(12) + .foregroundStyle(.white) + .background(.blue) + .clipShape(RoundedRectangle(cornerRadius: 15)) + } + .padding(.horizontal) + } + private func showVNDocumentCameraView() { Task { guard await isAuthorized else { return } } @@ -95,5 +108,11 @@ struct MainView: View { } #Preview { - MainView() + let config = ModelConfiguration(isStoredInMemoryOnly: true) + let container = try! ModelContainer(for: Scan.self, configurations: config) + + let scan = Scan(images: [UIImage(systemName: "questionmark")!]) + container.mainContext.insert(scan) + + return MainView().modelContainer(container) } diff --git a/JustScanIt/View/ScanItemView.swift b/JustScanIt/View/ScanItemView.swift index 1c15674..e26c7bb 100644 --- a/JustScanIt/View/ScanItemView.swift +++ b/JustScanIt/View/ScanItemView.swift @@ -96,6 +96,7 @@ struct ScanItemView: View { .resizable() .scaledToFit() .frame(maxHeight: 400) + .pinchToZoom() .onTapGesture { withAnimation(.easeInOut(duration: 0.3)) { isZoomed = false