From de46d1ac2eb46925be74bcfc5fdfc29217c48bd4 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Wed, 31 Mar 2021 12:28:53 -0600 Subject: [PATCH 1/4] Move pinch to zoom to new file --- Package.swift | 4 +- Sources/ImageViewer/ImageViewer.swift | 152 ------------------ Sources/ImageViewer/PinchToZoom.swift | 152 ++++++++++++++++++ .../ImageViewerRemote/ImageViewerRemote.swift | 152 +----------------- 4 files changed, 155 insertions(+), 305 deletions(-) create mode 100644 Sources/ImageViewer/PinchToZoom.swift diff --git a/Package.swift b/Package.swift index 2be5103..dbaea49 100644 --- a/Package.swift +++ b/Package.swift @@ -27,9 +27,9 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "ImageViewer", - dependencies: ["URLImage"]), + dependencies: []), .target( name: "ImageViewerRemote", - dependencies: ["URLImage"]) + dependencies: ["ImageViewer", "URLImage"]) ] ) diff --git a/Sources/ImageViewer/ImageViewer.swift b/Sources/ImageViewer/ImageViewer.swift index 1b2a2e2..9353f4b 100644 --- a/Sources/ImageViewer/ImageViewer.swift +++ b/Sources/ImageViewer/ImageViewer.swift @@ -135,155 +135,3 @@ public struct ImageViewer: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } - - -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) - .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) - } -} - -extension View { - func pinchToZoom() -> some View { - self.modifier(PinchToZoom()) - } -} diff --git a/Sources/ImageViewer/PinchToZoom.swift b/Sources/ImageViewer/PinchToZoom.swift new file mode 100644 index 0000000..2730092 --- /dev/null +++ b/Sources/ImageViewer/PinchToZoom.swift @@ -0,0 +1,152 @@ +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) + .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) + } +} + +extension View { + public func pinchToZoom() -> some View { + self.modifier(PinchToZoom()) + } +} diff --git a/Sources/ImageViewerRemote/ImageViewerRemote.swift b/Sources/ImageViewerRemote/ImageViewerRemote.swift index 1f017fd..ff7fead 100644 --- a/Sources/ImageViewerRemote/ImageViewerRemote.swift +++ b/Sources/ImageViewerRemote/ImageViewerRemote.swift @@ -1,5 +1,6 @@ import SwiftUI import UIKit +import ImageViewer import URLImage import Combine @@ -160,157 +161,6 @@ public struct ImageViewerRemote: View { } } -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) - .overlay(PinchZoom(scale: $scale, anchor: $anchor, offset: $offset, isPinching: $isPinching)) - } -} - -extension View { - func pinchToZoom() -> some View { - self.modifier(PinchToZoom()) - } -} - From fe52b15919f480ac0487acfa1013b30fadb7403c Mon Sep 17 00:00:00 2001 From: Isvvc Date: Wed, 31 Mar 2021 12:30:46 -0600 Subject: [PATCH 2/4] Move ImageLoader to new file --- Sources/ImageViewerRemote/ImageLoader.swift | 40 +++++++++++++++++ .../ImageViewerRemote/ImageViewerRemote.swift | 45 ------------------- 2 files changed, 40 insertions(+), 45 deletions(-) create mode 100644 Sources/ImageViewerRemote/ImageLoader.swift diff --git a/Sources/ImageViewerRemote/ImageLoader.swift b/Sources/ImageViewerRemote/ImageLoader.swift new file mode 100644 index 0000000..da3765a --- /dev/null +++ b/Sources/ImageViewerRemote/ImageLoader.swift @@ -0,0 +1,40 @@ +import SwiftUI +import Combine + +class ImageLoader: ObservableObject { + @Published var image: UIImage? + private let url: Binding + private var cancellable: AnyCancellable? + + func getURLRequest(url: String) -> URLRequest { + let url = URL(string: url) ?? URL(string: "https://via.placeholder.com/150.png")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + + return request; + } + + init(url: Binding) { + self.url = url + + if(url.wrappedValue.count > 0) { + load() + } + } + + deinit { + cancellable?.cancel() + } + + func load() { + cancellable = URLSession.shared.dataTaskPublisher(for: getURLRequest(url: self.url.wrappedValue)) + .map { UIImage(data: $0.data) } + .replaceError(with: nil) + .receive(on: DispatchQueue.main) + .assign(to: \.image, on: self) + } + + func cancel() { + cancellable?.cancel() + } +} diff --git a/Sources/ImageViewerRemote/ImageViewerRemote.swift b/Sources/ImageViewerRemote/ImageViewerRemote.swift index ff7fead..56c6d1b 100644 --- a/Sources/ImageViewerRemote/ImageViewerRemote.swift +++ b/Sources/ImageViewerRemote/ImageViewerRemote.swift @@ -1,8 +1,6 @@ import SwiftUI -import UIKit import ImageViewer import URLImage -import Combine @available(iOS 13.0, *) public struct ImageViewerRemote: View { @@ -160,46 +158,3 @@ public struct ImageViewerRemote: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } - - - - - - -class ImageLoader: ObservableObject { - @Published var image: UIImage? - private let url: Binding - private var cancellable: AnyCancellable? - - func getURLRequest(url: String) -> URLRequest { - let url = URL(string: url) ?? URL(string: "https://via.placeholder.com/150.png")! - var request = URLRequest(url: url) - request.httpMethod = "GET" - - return request; - } - - init(url: Binding) { - self.url = url - - if(url.wrappedValue.count > 0) { - load() - } - } - - deinit { - cancellable?.cancel() - } - - func load() { - cancellable = URLSession.shared.dataTaskPublisher(for: getURLRequest(url: self.url.wrappedValue)) - .map { UIImage(data: $0.data) } - .replaceError(with: nil) - .receive(on: DispatchQueue.main) - .assign(to: \.image, on: self) - } - - func cancel() { - cancellable?.cancel() - } -} From 5da62c60c113c9af13cefaa8c3d5b92ac193ab3f Mon Sep 17 00:00:00 2001 From: Isvvc Date: Wed, 31 Mar 2021 13:31:08 -0600 Subject: [PATCH 3/4] Separate image as separate view from image viewer This reduces redundant code. Also replace many state and binding properties with simple properties --- Sources/ImageViewer/ImageDetails.swift | 25 ++++ Sources/ImageViewer/ImageView.swift | 82 ++++++++++++ Sources/ImageViewer/ImageViewer.swift | 103 ++------------- Sources/ImageViewerRemote/ImageLoader.swift | 8 +- .../ImageViewerRemote/ImageViewerRemote.swift | 122 ++++-------------- 5 files changed, 145 insertions(+), 195 deletions(-) create mode 100644 Sources/ImageViewer/ImageDetails.swift create mode 100644 Sources/ImageViewer/ImageView.swift diff --git a/Sources/ImageViewer/ImageDetails.swift b/Sources/ImageViewer/ImageDetails.swift new file mode 100644 index 0000000..da518c8 --- /dev/null +++ b/Sources/ImageViewer/ImageDetails.swift @@ -0,0 +1,25 @@ +import SwiftUI + +public struct ImageDetails { + var image: Image? + var aspectRatio: CGFloat? + var caption: String? + + public init(image: Image?, aspectRatio: CGFloat? = nil, caption: String? = nil) { + self.image = image + self.aspectRatio = aspectRatio + self.caption = caption + } +} + +public struct ImageDetailsRemote { + public var imageURL: String + var aspectRatio: CGFloat? + var caption: String? + + public init(imageURL: String, aspectRatio: CGFloat? = nil, caption: String? = nil) { + self.imageURL = imageURL + self.aspectRatio = aspectRatio + self.caption = caption + } +} diff --git a/Sources/ImageViewer/ImageView.swift b/Sources/ImageViewer/ImageView.swift new file mode 100644 index 0000000..50239b7 --- /dev/null +++ b/Sources/ImageViewer/ImageView.swift @@ -0,0 +1,82 @@ +import SwiftUI + +public struct ImageView: View { + + var imageDetails: ImageDetails + @Binding var viewerShown: Bool + + @State var dragOffset: CGSize = .zero + @State var dragOffsetPredicted: CGSize = .zero + + public init(imageDetails: ImageDetails, viewerShown: Binding) { + self.imageDetails = imageDetails + _viewerShown = viewerShown + } + + public var body: some View { + VStack { + ZStack { + (imageDetails.image ?? Image(systemName: "questionmark.diamond")) + .resizable() + .aspectRatio(imageDetails.aspectRatio, contentMode: .fit) + .offset(x: self.dragOffset.width, y: self.dragOffset.height) + .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) + .pinchToZoom() + .gesture(DragGesture() + .onChanged { value in + self.dragOffset = value.translation + self.dragOffsetPredicted = value.predictedEndTranslation + } + .onEnded { value in + if((abs(self.dragOffset.height) + abs(self.dragOffset.width) > 570) || ((abs(self.dragOffsetPredicted.height)) / (abs(self.dragOffset.height)) > 3) || ((abs(self.dragOffsetPredicted.width)) / (abs(self.dragOffset.width))) > 3) { + withAnimation(.spring()) { + self.dragOffset = self.dragOffsetPredicted + } + self.viewerShown = false + + return + } + withAnimation(.interactiveSpring()) { + self.dragOffset = .zero + } + } + ) + + if let caption = imageDetails.caption { + VStack { + Spacer() + + VStack { + Spacer() + + HStack { + Spacer() + + Text(caption) + .foregroundColor(.white) + .multilineTextAlignment(.center) + + Spacer() + } + } + .padding() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(red: 0.12, green: 0.12, blue: 0.12, opacity: (1.0 - Double(abs(self.dragOffset.width) + abs(self.dragOffset.height)) / 1000)).edgesIgnoringSafeArea(.all)) + .zIndex(1) + } +} + +struct SwiftUIView_Previews: PreviewProvider { + + static var details = ImageDetails(image: Image(systemName: "gear"), caption: "Gear") + + static var previews: some View { + ImageView(imageDetails: details, viewerShown: .constant(true)) + } +} diff --git a/Sources/ImageViewer/ImageViewer.swift b/Sources/ImageViewer/ImageViewer.swift index 9353f4b..c3385d8 100644 --- a/Sources/ImageViewer/ImageViewer.swift +++ b/Sources/ImageViewer/ImageViewer.swift @@ -1,46 +1,23 @@ import SwiftUI -import UIKit @available(iOS 13.0, *) public struct ImageViewer: View { - @Binding var viewerShown: Bool - @Binding var image: Image - @Binding var imageOpt: Image? - @State var caption: Text? - @State var closeButtonTopRight: Bool? - var aspectRatio: Binding? + var imageDetails: ImageDetails + var closeButtonTopRight: Bool - @State var dragOffset: CGSize = CGSize.zero - @State var dragOffsetPredicted: CGSize = CGSize.zero + @Binding var viewerShown: Bool - public init(image: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { - _image = image + public init(imageDetails: ImageDetails, viewerShown: Binding, closeButtonTopRight: Bool = false) { + self.imageDetails = imageDetails _viewerShown = viewerShown - _imageOpt = .constant(nil) - self.aspectRatio = aspectRatio - _caption = State(initialValue: caption) - _closeButtonTopRight = State(initialValue: closeButtonTopRight) + self.closeButtonTopRight = closeButtonTopRight } - public init(image: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { - _image = .constant(Image(systemName: "")) - _imageOpt = image - _viewerShown = viewerShown - self.aspectRatio = aspectRatio - _caption = State(initialValue: caption) - _closeButtonTopRight = State(initialValue: closeButtonTopRight) + public init(image: Image?, viewerShown: Binding, aspectRatio: CGFloat? = nil, caption: String? = nil, closeButtonTopRight: Bool = false) { + self.init(imageDetails: ImageDetails(image: image, aspectRatio: aspectRatio, caption: caption), viewerShown: viewerShown, closeButtonTopRight: closeButtonTopRight) } - func getImage() -> Image { - if(self.imageOpt == nil) { - return self.image - } - else { - return self.imageOpt ?? Image(systemName: "questionmark.diamond") - } - } - @ViewBuilder public var body: some View { VStack { @@ -49,7 +26,7 @@ public struct ImageViewer: View { VStack { HStack { - if self.closeButtonTopRight == true { + if closeButtonTopRight { Spacer() } @@ -59,7 +36,7 @@ public struct ImageViewer: View { .font(.system(size: UIFontMetrics.default.scaledValue(for: 24))) } - if self.closeButtonTopRight != true { + if !closeButtonTopRight { Spacer() } } @@ -69,67 +46,9 @@ public struct ImageViewer: View { .padding() .zIndex(2) - VStack { - ZStack { - self.getImage() - .resizable() - .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) - .offset(x: self.dragOffset.width, y: self.dragOffset.height) - .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) - .pinchToZoom() - .gesture(DragGesture() - .onChanged { value in - self.dragOffset = value.translation - self.dragOffsetPredicted = value.predictedEndTranslation - } - .onEnded { value in - if((abs(self.dragOffset.height) + abs(self.dragOffset.width) > 570) || ((abs(self.dragOffsetPredicted.height)) / (abs(self.dragOffset.height)) > 3) || ((abs(self.dragOffsetPredicted.width)) / (abs(self.dragOffset.width))) > 3) { - withAnimation(.spring()) { - self.dragOffset = self.dragOffsetPredicted - } - self.viewerShown = false - - return - } - withAnimation(.interactiveSpring()) { - self.dragOffset = .zero - } - } - ) - - if(self.caption != nil) { - VStack { - Spacer() - - VStack { - Spacer() - - HStack { - Spacer() - - self.caption - .foregroundColor(.white) - .multilineTextAlignment(.center) - - Spacer() - } - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(red: 0.12, green: 0.12, blue: 0.12, opacity: (1.0 - Double(abs(self.dragOffset.width) + abs(self.dragOffset.height)) / 1000)).edgesIgnoringSafeArea(.all)) - .zIndex(1) + ImageView(imageDetails: imageDetails, viewerShown: $viewerShown) } .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) - .onAppear() { - self.dragOffset = .zero - self.dragOffsetPredicted = .zero - } } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Sources/ImageViewerRemote/ImageLoader.swift b/Sources/ImageViewerRemote/ImageLoader.swift index da3765a..9a57487 100644 --- a/Sources/ImageViewerRemote/ImageLoader.swift +++ b/Sources/ImageViewerRemote/ImageLoader.swift @@ -3,7 +3,7 @@ import Combine class ImageLoader: ObservableObject { @Published var image: UIImage? - private let url: Binding + private let url: String private var cancellable: AnyCancellable? func getURLRequest(url: String) -> URLRequest { @@ -14,10 +14,10 @@ class ImageLoader: ObservableObject { return request; } - init(url: Binding) { + init(url: String) { self.url = url - if(url.wrappedValue.count > 0) { + if !url.isEmpty { load() } } @@ -27,7 +27,7 @@ class ImageLoader: ObservableObject { } func load() { - cancellable = URLSession.shared.dataTaskPublisher(for: getURLRequest(url: self.url.wrappedValue)) + cancellable = URLSession.shared.dataTaskPublisher(for: getURLRequest(url: url)) .map { UIImage(data: $0.data) } .replaceError(with: nil) .receive(on: DispatchQueue.main) diff --git a/Sources/ImageViewerRemote/ImageViewerRemote.swift b/Sources/ImageViewerRemote/ImageViewerRemote.swift index 56c6d1b..5ea9310 100644 --- a/Sources/ImageViewerRemote/ImageViewerRemote.swift +++ b/Sources/ImageViewerRemote/ImageViewerRemote.swift @@ -4,35 +4,35 @@ import URLImage @available(iOS 13.0, *) public struct ImageViewerRemote: View { - @Binding var viewerShown: Bool - @Binding var imageURL: String - @State var httpHeaders: [String: String]? - @State var disableCache: Bool? - @State var caption: Text? - @State var closeButtonTopRight: Bool? - var aspectRatio: Binding? + var imageDetails: ImageDetailsRemote + var disableCache: Bool + var closeButtonTopRight: Bool? - @State var dragOffset: CGSize = CGSize.zero - @State var dragOffsetPredicted: CGSize = CGSize.zero + @Binding var viewerShown: Bool @ObservedObject var loader: ImageLoader - public init(imageURL: Binding, viewerShown: Binding, aspectRatio: Binding? = nil, disableCache: Bool? = nil, caption: Text? = nil, closeButtonTopRight: Bool? = false) { - _imageURL = imageURL + public init(imageDetails: ImageDetailsRemote, viewerShown: Binding, disableCache: Bool = false, closeButtonTopRight: Bool? = nil) { + self.imageDetails = imageDetails + _viewerShown = viewerShown + self.disableCache = disableCache + self.closeButtonTopRight = closeButtonTopRight + loader = ImageLoader(url: imageDetails.imageURL) + } + + public init(imageURL: String, viewerShown: Binding, aspectRatio: CGFloat? = nil, caption: String? = nil, disableCache: Bool = false, closeButtonTopRight: Bool? = nil) { + imageDetails = ImageDetailsRemote(imageURL: imageURL, aspectRatio: aspectRatio, caption: caption) _viewerShown = viewerShown - _disableCache = State(initialValue: disableCache) - self.aspectRatio = aspectRatio - _caption = State(initialValue: caption) - _closeButtonTopRight = State(initialValue: closeButtonTopRight) - + self.disableCache = disableCache + self.closeButtonTopRight = closeButtonTopRight loader = ImageLoader(url: imageURL) } - + @ViewBuilder public var body: some View { VStack { - if(viewerShown && imageURL.count > 0) { + if(viewerShown && !imageDetails.imageURL.isEmpty) { ZStack { VStack { HStack { @@ -60,99 +60,23 @@ public struct ImageViewerRemote: View { VStack { ZStack { - if(self.disableCache == nil || self.disableCache == false) { - URLImage(url: URL(string: self.imageURL) ?? URL(string: "https://via.placeholder.com/150.png")!, content: { image in - image - .resizable() - .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) - .offset(x: self.dragOffset.width, y: self.dragOffset.height) - .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) - .pinchToZoom() - .gesture(DragGesture() - .onChanged { value in - self.dragOffset = value.translation - self.dragOffsetPredicted = value.predictedEndTranslation - } - .onEnded { value in - if((abs(self.dragOffset.height) + abs(self.dragOffset.width) > 570) || ((abs(self.dragOffsetPredicted.height)) / (abs(self.dragOffset.height)) > 3) || ((abs(self.dragOffsetPredicted.width)) / (abs(self.dragOffset.width))) > 3) { - withAnimation(.spring()) { - self.dragOffset = self.dragOffsetPredicted - } - self.viewerShown = false - return - } - withAnimation(.interactiveSpring()) { - self.dragOffset = .zero - } - } - ) + if !disableCache { + URLImage(url: URL(string: imageDetails.imageURL) ?? URL(string: "https://via.placeholder.com/150.png")!, content: { image in + ImageView(imageDetails: ImageDetails(image: image), viewerShown: $viewerShown) }) } else { - if loader.image != nil { - Image(uiImage: loader.image!) - .resizable() - .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) - .offset(x: self.dragOffset.width, y: self.dragOffset.height) - .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) - .pinchToZoom() - .gesture(DragGesture() - .onChanged { value in - self.dragOffset = value.translation - self.dragOffsetPredicted = value.predictedEndTranslation - } - .onEnded { value in - if((abs(self.dragOffset.height) + abs(self.dragOffset.width) > 570) || ((abs(self.dragOffsetPredicted.height)) / (abs(self.dragOffset.height)) > 3) || ((abs(self.dragOffsetPredicted.width)) / (abs(self.dragOffset.width))) > 3) { - withAnimation(.spring()) { - self.dragOffset = self.dragOffsetPredicted - } - self.viewerShown = false - return - } - withAnimation(.interactiveSpring()) { - self.dragOffset = .zero - } - } - ) + if let image = loader.image { + ImageView(imageDetails: ImageDetails(image: Image(uiImage: image)), viewerShown: $viewerShown) } else { Text(":/") } } - - if(self.caption != nil) { - VStack { - Spacer() - - VStack { - Spacer() - - HStack { - Spacer() - - self.caption - .foregroundColor(.white) - .multilineTextAlignment(.center) - - Spacer() - } - } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } } } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(red: 0.12, green: 0.12, blue: 0.12, opacity: (1.0 - Double(abs(self.dragOffset.width) + abs(self.dragOffset.height)) / 1000)).edgesIgnoringSafeArea(.all)) - .zIndex(1) } .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2))) - .onAppear() { - self.dragOffset = .zero - self.dragOffsetPredicted = .zero - } } } .frame(maxWidth: .infinity, maxHeight: .infinity) From eaba59f7f1f9d973c604d8df3177424fdd579653 Mon Sep 17 00:00:00 2001 From: Isvvc Date: Wed, 31 Mar 2021 13:43:03 -0600 Subject: [PATCH 4/4] Restore animation when releasing pinch See #27 --- Sources/ImageViewer/PinchToZoom.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/ImageViewer/PinchToZoom.swift b/Sources/ImageViewer/PinchToZoom.swift index 2730092..b205d94 100644 --- a/Sources/ImageViewer/PinchToZoom.swift +++ b/Sources/ImageViewer/PinchToZoom.swift @@ -69,10 +69,12 @@ class PinchZoomView: UIView { 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 + withAnimation(.interactiveSpring()) { + isPinching = false + scale = 1.0 + anchor = .center + offset = .zero + } default: break }