|
1 | 1 | import SwiftUI
|
2 | 2 | import UIKit
|
3 | 3 | import URLImage
|
| 4 | +import Combine |
4 | 5 |
|
5 | 6 | @available(iOS 13.0, *)
|
6 | 7 | public struct ImageViewerRemote: View {
|
7 | 8 | @Binding var viewerShown: Bool
|
8 | 9 | @Binding var imageURL: String
|
9 | 10 | @State var httpHeaders: [String: String]?
|
| 11 | + @State var disableCache: Bool? |
10 | 12 |
|
11 | 13 | var aspectRatio: Binding<CGFloat>?
|
12 | 14 |
|
13 | 15 | @State var dragOffset: CGSize = CGSize.zero
|
14 | 16 | @State var dragOffsetPredicted: CGSize = CGSize.zero
|
15 | 17 |
|
16 |
| - public init(imageURL: Binding<String>, viewerShown: Binding<Bool>, httpHeaders: [String: String]? = nil, aspectRatio: Binding<CGFloat>? = nil) { |
| 18 | + public init(imageURL: Binding<String>, viewerShown: Binding<Bool>, httpHeaders: [String: String]? = nil, aspectRatio: Binding<CGFloat>? = nil, disableCache: Bool? = nil) { |
17 | 19 | _imageURL = imageURL
|
18 | 20 | _viewerShown = viewerShown
|
19 | 21 | _httpHeaders = State(initialValue: httpHeaders)
|
| 22 | + _disableCache = State(initialValue: disableCache) |
20 | 23 | self.aspectRatio = aspectRatio
|
21 | 24 | }
|
22 | 25 |
|
@@ -62,30 +65,38 @@ public struct ImageViewerRemote: View {
|
62 | 65 | .zIndex(2)
|
63 | 66 |
|
64 | 67 | VStack {
|
65 |
| - URLImage(getURLRequest(url: self.imageURL, headers: self.httpHeaders)) { proxy in |
66 |
| - proxy.image |
67 |
| - .resizable() |
68 |
| - .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) |
69 |
| - .offset(x: self.dragOffset.width, y: self.dragOffset.height) |
70 |
| - .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) |
71 |
| - .pinchToZoom() |
72 |
| - .gesture(DragGesture() |
73 |
| - .onChanged { value in |
74 |
| - self.dragOffset = value.translation |
75 |
| - self.dragOffsetPredicted = value.predictedEndTranslation |
76 |
| - } |
77 |
| - .onEnded { value in |
78 |
| - 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) { |
79 |
| - self.viewerShown = false |
80 |
| - return |
| 68 | + if(self.disableCache == nil || self.disableCache == false) { |
| 69 | + URLImage(getURLRequest(url: self.imageURL, headers: self.httpHeaders)) { proxy in |
| 70 | + proxy.image |
| 71 | + .resizable() |
| 72 | + .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) |
| 73 | + .offset(x: self.dragOffset.width, y: self.dragOffset.height) |
| 74 | + .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) |
| 75 | + .pinchToZoom() |
| 76 | + .gesture(DragGesture() |
| 77 | + .onChanged { value in |
| 78 | + self.dragOffset = value.translation |
| 79 | + self.dragOffsetPredicted = value.predictedEndTranslation |
81 | 80 | }
|
82 |
| - self.dragOffset = .zero |
83 |
| - } |
84 |
| - ) |
| 81 | + .onEnded { value in |
| 82 | + 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) { |
| 83 | + self.viewerShown = false |
| 84 | + return |
| 85 | + } |
| 86 | + self.dragOffset = .zero |
| 87 | + } |
| 88 | + ) |
| 89 | + } |
| 90 | + .frame(maxWidth: .infinity, maxHeight: .infinity) |
| 91 | + .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)) |
| 92 | + .zIndex(1) |
| 93 | + } |
| 94 | + else { |
| 95 | + AsyncImage( |
| 96 | + url: getURLRequest(url: self.imageURL, headers: self.httpHeaders), |
| 97 | + placeholder: Text("Loading ..."), aspectRatio: self.aspectRatio, dragOffset: self.$dragOffset, dragOffsetPredicted: self.$dragOffsetPredicted, viewerShown: self.$viewerShown |
| 98 | + ) |
85 | 99 | }
|
86 |
| - .frame(maxWidth: .infinity, maxHeight: .infinity) |
87 |
| - .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)) |
88 |
| - .zIndex(1) |
89 | 100 | }
|
90 | 101 | }
|
91 | 102 | .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.2)))
|
@@ -250,3 +261,88 @@ extension View {
|
250 | 261 | self.modifier(PinchToZoom())
|
251 | 262 | }
|
252 | 263 | }
|
| 264 | + |
| 265 | + |
| 266 | + |
| 267 | + |
| 268 | + |
| 269 | + |
| 270 | +class ImageLoader: ObservableObject { |
| 271 | + @Published var image: UIImage? |
| 272 | + private let url: URLRequest |
| 273 | + private var cancellable: AnyCancellable? |
| 274 | + |
| 275 | + init(url: URLRequest) { |
| 276 | + self.url = url |
| 277 | + } |
| 278 | + |
| 279 | + deinit { |
| 280 | + cancellable?.cancel() |
| 281 | + } |
| 282 | + |
| 283 | + func load() { |
| 284 | + cancellable = URLSession.shared.dataTaskPublisher(for: url) |
| 285 | + .map { UIImage(data: $0.data) } |
| 286 | + .replaceError(with: nil) |
| 287 | + .receive(on: DispatchQueue.main) |
| 288 | + .assign(to: \.image, on: self) |
| 289 | + } |
| 290 | + |
| 291 | + func cancel() { |
| 292 | + cancellable?.cancel() |
| 293 | + } |
| 294 | +} |
| 295 | + |
| 296 | +struct AsyncImage<Placeholder: View>: View { |
| 297 | + @ObservedObject private var loader: ImageLoader |
| 298 | + |
| 299 | + private let placeholder: Placeholder? |
| 300 | + |
| 301 | + var aspectRatio: Binding<CGFloat>? |
| 302 | + @Binding var dragOffset: CGSize |
| 303 | + @Binding var dragOffsetPredicted: CGSize |
| 304 | + @Binding var viewerShown: Bool |
| 305 | + |
| 306 | + init(url: URLRequest, placeholder: Placeholder? = nil, aspectRatio: Binding<CGFloat>?, dragOffset: Binding<CGSize>, dragOffsetPredicted: Binding<CGSize>, viewerShown: Binding<Bool>) { |
| 307 | + loader = ImageLoader(url: url) |
| 308 | + self.placeholder = placeholder |
| 309 | + self.aspectRatio = aspectRatio |
| 310 | + _dragOffset = dragOffset |
| 311 | + _dragOffsetPredicted = dragOffsetPredicted |
| 312 | + _viewerShown = viewerShown |
| 313 | + } |
| 314 | + |
| 315 | + var body: some View { |
| 316 | + image |
| 317 | + .onAppear(perform: loader.load) |
| 318 | + .onDisappear(perform: loader.cancel) |
| 319 | + } |
| 320 | + |
| 321 | + private var image: some View { |
| 322 | + Group { |
| 323 | + if loader.image != nil { |
| 324 | + Image(uiImage: loader.image!) |
| 325 | + .resizable() |
| 326 | + .aspectRatio(self.aspectRatio?.wrappedValue, contentMode: .fit) |
| 327 | + .offset(x: self.dragOffset.width, y: self.dragOffset.height) |
| 328 | + .rotationEffect(.init(degrees: Double(self.dragOffset.width / 30))) |
| 329 | + .pinchToZoom() |
| 330 | + .gesture(DragGesture() |
| 331 | + .onChanged { value in |
| 332 | + self.dragOffset = value.translation |
| 333 | + self.dragOffsetPredicted = value.predictedEndTranslation |
| 334 | + } |
| 335 | + .onEnded { value in |
| 336 | + 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) { |
| 337 | + self.viewerShown = false |
| 338 | + return |
| 339 | + } |
| 340 | + self.dragOffset = .zero |
| 341 | + } |
| 342 | + ) |
| 343 | + } else { |
| 344 | + placeholder |
| 345 | + } |
| 346 | + } |
| 347 | + } |
| 348 | +} |
0 commit comments