-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1413 from planetary-social/uiscrollview-image-viewer
Display images with new image viewer (UIScrollView version)
- Loading branch information
Showing
11 changed files
with
308 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
Nos/Assets/Colors.xcassets/button-close-background.colorset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"colors" : [ | ||
{ | ||
"color" : { | ||
"color-space" : "srgb", | ||
"components" : { | ||
"alpha" : "1.000", | ||
"blue" : "0x24", | ||
"green" : "0x0E", | ||
"red" : "0x16" | ||
} | ||
}, | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
Nos/Assets/Colors.xcassets/button-close-foreground.colorset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"colors" : [ | ||
{ | ||
"color" : { | ||
"color-space" : "srgb", | ||
"components" : { | ||
"alpha" : "1.000", | ||
"blue" : "0xC6", | ||
"green" : "0x81", | ||
"red" : "0x9A" | ||
} | ||
}, | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
Nos/Assets/Colors.xcassets/image-viewer-background.colorset/Contents.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"colors" : [ | ||
{ | ||
"color" : { | ||
"color-space" : "srgb", | ||
"components" : { | ||
"alpha" : "1.000", | ||
"blue" : "0x00", | ||
"green" : "0x00", | ||
"red" : "0x00" | ||
} | ||
}, | ||
"idiom" : "universal" | ||
} | ||
], | ||
"info" : { | ||
"author" : "xcode", | ||
"version" : 1 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import SDWebImageSwiftUI | ||
import SwiftUI | ||
|
||
/// A viewer for images. Supports full-screen zoom and panning. | ||
struct ImageViewer: View { | ||
/// The URL of the image to display. | ||
let url: URL | ||
|
||
@Environment(\.dismiss) private var dismiss | ||
|
||
var body: some View { | ||
ZStack { | ||
Color.imageViewerBackground | ||
|
||
ZoomableContainer { | ||
WebImage(url: url) | ||
.resizable() | ||
.aspectRatio(contentMode: .fit) | ||
} | ||
|
||
ZStack(alignment: .topLeading) { | ||
Color.clear | ||
|
||
Button { | ||
dismiss() | ||
} label: { | ||
Image(systemName: "xmark") | ||
.symbolVariant(.fill.circle) | ||
.symbolRenderingMode(.palette) | ||
.foregroundStyle(Color.buttonCloseForeground, Color.buttonCloseBackground) | ||
.font(.largeTitle) | ||
} | ||
.padding() | ||
} | ||
} | ||
.ignoresSafeArea() | ||
} | ||
} | ||
|
||
#Preview { | ||
ImageViewer( | ||
url: URL( | ||
string: "https://image.nostr.build/92d0ed5e3c53fa33e379f0982d52058f0dde98f0c287669fd1e7c5b4b86b5dbb.jpg" | ||
)! | ||
) | ||
} | ||
|
||
#Preview { | ||
ImageViewer( | ||
url: URL( | ||
string: "https://images.unsplash.com/photo-1715686529501-e097bd9caea7" | ||
)! | ||
) | ||
} | ||
|
||
#Preview { | ||
ImageViewer( | ||
url: URL( | ||
string: "https://images.unsplash.com/photo-1723160004469-1b34c81272f3" | ||
)! | ||
) | ||
} | ||
|
||
#Preview { | ||
ImageViewer( | ||
url: URL( | ||
string: "https://image.nostr.build/9640e78f03afc4927d80a15fd1c4bd1404dc654a8663efb92cc9ee1b8b0719a3.jpg" | ||
)! | ||
) | ||
} | ||
|
||
#Preview { | ||
ImageViewer( | ||
url: URL( | ||
string: "https://images.unsplash.com/photo-1716783841007-7de314270444" | ||
)! | ||
) | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import SwiftUI | ||
|
||
/// A container that allows its content to be zoomed. | ||
/// - Note: Thanks, [Ido](https://stackoverflow.com/users/8157190/ido) for your | ||
/// [answer](https://stackoverflow.com/a/76649224) on StackOverflow! | ||
struct ZoomableContainer<ContainerContent: View>: View { | ||
let content: ContainerContent | ||
private let maxAllowedScale = 4.0 | ||
|
||
@State private var currentScale: CGFloat = 1.0 | ||
@State private var tapLocation: CGPoint = .zero | ||
|
||
init(@ViewBuilder content: () -> ContainerContent) { | ||
self.content = content() | ||
} | ||
|
||
func doubleTapAction(location: CGPoint) { | ||
tapLocation = location | ||
currentScale = currentScale == 1.0 ? maxAllowedScale : 1.0 | ||
} | ||
|
||
var body: some View { | ||
ZoomableScrollView(maxAllowedScale: maxAllowedScale, scale: $currentScale, tapLocation: $tapLocation) { | ||
content | ||
} | ||
.onTapGesture(count: 2, perform: doubleTapAction) | ||
} | ||
} | ||
|
||
fileprivate struct ZoomableScrollView<ScrollViewContent: View>: UIViewRepresentable { | ||
private var content: ScrollViewContent | ||
private let maxAllowedScale: CGFloat | ||
|
||
@Binding private var currentScale: CGFloat | ||
@Binding private var tapLocation: CGPoint | ||
|
||
init( | ||
maxAllowedScale: CGFloat, | ||
scale: Binding<CGFloat>, | ||
tapLocation: Binding<CGPoint>, | ||
@ViewBuilder content: () -> ScrollViewContent | ||
) { | ||
self.maxAllowedScale = maxAllowedScale | ||
_currentScale = scale | ||
_tapLocation = tapLocation | ||
self.content = content() | ||
} | ||
|
||
func makeUIView(context: Context) -> UIScrollView { | ||
// Setup the UIScrollView | ||
let scrollView = UIScrollView() | ||
scrollView.delegate = context.coordinator // for viewForZooming(in:) | ||
scrollView.maximumZoomScale = maxAllowedScale | ||
scrollView.minimumZoomScale = 1 | ||
scrollView.bouncesZoom = true | ||
scrollView.showsHorizontalScrollIndicator = false | ||
scrollView.showsVerticalScrollIndicator = false | ||
scrollView.clipsToBounds = false | ||
|
||
// Create a UIHostingController to hold our SwiftUI content | ||
let hostedView = context.coordinator.hostingController.view! | ||
hostedView.translatesAutoresizingMaskIntoConstraints = true | ||
hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight] | ||
hostedView.frame = scrollView.bounds | ||
hostedView.backgroundColor = .clear | ||
scrollView.addSubview(hostedView) | ||
|
||
return scrollView | ||
} | ||
|
||
func makeCoordinator() -> Coordinator { | ||
Coordinator(hostingController: UIHostingController(rootView: content), scale: $currentScale) | ||
} | ||
|
||
func updateUIView(_ uiView: UIScrollView, context: Context) { | ||
// Update the hosting controller's SwiftUI content | ||
context.coordinator.hostingController.rootView = content | ||
|
||
if uiView.zoomScale > uiView.minimumZoomScale { // Scale out | ||
uiView.setZoomScale(currentScale, animated: true) | ||
} else if tapLocation != .zero { // Scale in to a specific point | ||
uiView.zoom(to: zoomRect(for: uiView, scale: uiView.maximumZoomScale, center: tapLocation), animated: true) | ||
// Reset the location to prevent scaling to it in case of a negative scale (manual pinch) | ||
// Use the main thread to prevent unexpected behavior | ||
DispatchQueue.main.async { tapLocation = .zero } | ||
} | ||
|
||
assert(context.coordinator.hostingController.view.superview == uiView) | ||
} | ||
|
||
// MARK: - Utils | ||
|
||
func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect { | ||
let scrollViewSize = scrollView.bounds.size | ||
|
||
let width = scrollViewSize.width / scale | ||
let height = scrollViewSize.height / scale | ||
let xPosition = center.x - (width / 2.0) | ||
let yPosition = center.y - (height / 2.0) | ||
|
||
return CGRect(x: xPosition, y: yPosition, width: width, height: height) | ||
} | ||
|
||
// MARK: - Coordinator | ||
|
||
class Coordinator: NSObject, UIScrollViewDelegate { | ||
var hostingController: UIHostingController<ScrollViewContent> | ||
@Binding var currentScale: CGFloat | ||
|
||
init(hostingController: UIHostingController<ScrollViewContent>, scale: Binding<CGFloat>) { | ||
self.hostingController = hostingController | ||
_currentScale = scale | ||
} | ||
|
||
func viewForZooming(in scrollView: UIScrollView) -> UIView? { | ||
hostingController.view | ||
} | ||
|
||
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { | ||
currentScale = scale | ||
} | ||
} | ||
} |
Oops, something went wrong.