commit 98c24147e80a229755f8268d2333c0962f3d0771
parent d1e7de5dcbae376ea920d3b83fc37b5cda2fe0d0
Author: OlegAba <mail@olegaba.com>
Date: Thu, 19 Jan 2023 21:25:47 -0500
Drastically improve image viewer
Changelog-Added: Drastically improved image viewer
Closes: #349
Diffstat:
3 files changed, 181 insertions(+), 107 deletions(-)
diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift
@@ -12,14 +12,14 @@ import Kingfisher
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
- let activityItems: [URL]
+ let activityItems: [URL?]
let callback: Callback? = nil
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
- activityItems: activityItems,
+ activityItems: activityItems as [Any],
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
@@ -32,7 +32,7 @@ struct ShareSheet: UIViewControllerRepresentable {
}
struct ImageContextMenuModifier: ViewModifier {
- let url: URL
+ let url: URL?
let image: UIImage?
@Binding var showShareSheet: Bool
@@ -64,131 +64,202 @@ struct ImageContextMenuModifier: ViewModifier {
}
}
-struct ImageView: View {
- let urls: [URL]
+private struct ImageContainerView: View {
- @Environment(\.presentationMode) var presentationMode
- //let pubkey: String
- //let profiles: Profiles
+ @ObservedObject var imageModel: KFImageModel
+
+ @State private var image: UIImage?
+ @State private var showShareSheet = false
+
+ init(url: URL?) {
+ self.imageModel = KFImageModel(
+ url: url,
+ fallbackUrl: nil,
+ maxByteSize: 2000000, // 2 MB
+ downsampleSize: CGSize(width: 400, height: 400)
+ )
+ }
+
+ private struct ImageHandler: ImageModifier {
+ @Binding var handler: UIImage?
+
+ func modify(_ image: UIImage) -> UIImage {
+ handler = image
+ return image
+ }
+ }
+
+ var body: some View {
+
+ KFAnimatedImage(imageModel.url)
+ .callbackQueue(.dispatch(.global(qos: .background)))
+ .processingQueue(.dispatch(.global(qos: .background)))
+ .cacheOriginalImage()
+ .configure { view in
+ view.framePreloadCount = 1
+ }
+ .scaleFactor(UIScreen.main.scale)
+ .loadDiskFileSynchronously()
+ .fade(duration: 0.1)
+ .imageModifier(ImageHandler(handler: $image))
+ .onFailure { _ in
+ imageModel.downloadFailed()
+ }
+ .id(imageModel.refreshID)
+ .clipped()
+ .modifier(ImageContextMenuModifier(url: imageModel.url, image: image, showShareSheet: $showShareSheet))
+ .sheet(isPresented: $showShareSheet) {
+ ShareSheet(activityItems: [imageModel.url])
+ }
+
+ // TODO: Update ImageCarousel with serializer and processor
+ // .serialize(by: imageModel.serializer)
+ // .setProcessor(imageModel.processor)
+
+ }
+}
+
+struct ZoomableScrollView<Content: View>: UIViewRepresentable {
+
+ private var content: Content
+
+ init(@ViewBuilder content: () -> Content) {
+ self.content = content()
+ }
- @GestureState private var scaleState: CGFloat = 1
- @GestureState private var offsetState = CGSize.zero
+ func makeUIView(context: Context) -> UIScrollView {
+ let scrollView = UIScrollView()
+ scrollView.delegate = context.coordinator
+ scrollView.maximumZoomScale = 20
+ scrollView.minimumZoomScale = 1
+ scrollView.bouncesZoom = true
+ scrollView.showsVerticalScrollIndicator = false
+ scrollView.showsHorizontalScrollIndicator = false
- @State private var offset = CGSize.zero
- @State private var scale: CGFloat = 1
+ let hostedView = context.coordinator.hostingController.view!
+ hostedView.translatesAutoresizingMaskIntoConstraints = true
+ hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ hostedView.frame = scrollView.bounds
+ hostedView.backgroundColor = .clear
+ scrollView.addSubview(hostedView)
- func resetStatus(){
- self.offset = CGSize.zero
- self.scale = 1
+ return scrollView
}
- var zoomGesture: some Gesture {
- MagnificationGesture()
- .updating($scaleState) { currentState, gestureState, _ in
- gestureState = currentState
- }
- .onEnded { value in
- scale *= value
- }
+ func makeCoordinator() -> Coordinator {
+ return Coordinator(hostingController: UIHostingController(rootView: self.content))
}
- var dragGesture: some Gesture {
- DragGesture()
- .updating($offsetState) { currentState, gestureState, _ in
- gestureState = currentState.translation
- }.onEnded { value in
- offset.height += value.translation.height
- offset.width += value.translation.width
- }
+ func updateUIView(_ uiView: UIScrollView, context: Context) {
+ context.coordinator.hostingController.rootView = self.content
+ assert(context.coordinator.hostingController.view.superview == uiView)
}
- var doubleTapGesture : some Gesture {
- TapGesture(count: 2).onEnded { value in
- resetStatus()
+ class Coordinator: NSObject, UIScrollViewDelegate {
+ var hostingController: UIHostingController<Content>
+
+ init(hostingController: UIHostingController<Content>) {
+ self.hostingController = hostingController
+ }
+
+ func viewForZooming(in scrollView: UIScrollView) -> UIView? {
+ return hostingController.view
}
}
+}
+
+struct ImageView: View {
- private struct ImageHandler: ImageModifier {
- @Binding var handler: UIImage?
-
- func modify(_ image: UIImage) -> UIImage {
- handler = image
- return image
+ let urls: [URL?]
+
+ @Environment(\.presentationMode) var presentationMode
+
+ @State private var selectedIndex = 0
+ @State var showMenu = true
+
+ var navBarView: some View {
+ VStack {
+ HStack {
+ Text(urls[selectedIndex]?.lastPathComponent ?? "")
+ .bold()
+
+ Spacer()
+
+ Button(action: {
+ presentationMode.wrappedValue.dismiss()
+ }, label: {
+ Image(systemName: "xmark")
+ })
+ }
+ .padding()
+
+ Divider()
+ .ignoresSafeArea()
}
+ .background(.regularMaterial)
}
-
- @State private var image: UIImage?
- @State private var showShareSheet = false
- func onShared(completed: Bool) -> Void {
- if (completed) {
- showShareSheet = false
+ var tabViewIndicator: some View {
+ HStack(spacing: 10) {
+ ForEach(urls.indices, id: \.self) { index in
+ Capsule()
+ .fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
+ .frame(width: 7, height: 7)
+ }
}
+ .padding()
+ .background(.regularMaterial)
+ .clipShape(Capsule())
}
var body: some View {
- ZStack(alignment: .topLeading) {
- Color("DamusDarkGrey") // Or Color("DamusBlack")
- .edgesIgnoringSafeArea(.all)
+ ZStack {
+ Color(.systemBackground)
+ .ignoresSafeArea()
- HStack() {
- Button {
- presentationMode.wrappedValue.dismiss()
- } label: {
- Image(systemName: "xmark")
- .foregroundColor(.white)
- .font(.largeTitle)
- .frame(width: 40, height: 40)
- .padding(20)
+ TabView(selection: $selectedIndex) {
+ ForEach(urls.indices, id: \.self) { index in
+ ZoomableScrollView {
+ ImageContainerView(url: urls[index])
+ .aspectRatio(contentMode: .fit)
+ }
+ .ignoresSafeArea()
+ .tag(index)
+ .modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
+ presentationMode.wrappedValue.dismiss()
+ }))
}
}
- .zIndex(1)
-
- VStack(alignment: .center) {
- //Spacer()
- //.frame(height: 120)
-
- TabView {
- ForEach(urls, id: \.absoluteString) { url in
- VStack{
- //Color("DamusDarkGrey")
- Text(url.lastPathComponent)
- .foregroundColor(Color("DamusWhite"))
-
- KFAnimatedImage(url)
- .configure { view in
- view.framePreloadCount = 3
- }
- .cacheOriginalImage()
- .imageModifier(ImageHandler(handler: $image))
- .loadDiskFileSynchronously()
- .scaleFactor(UIScreen.main.scale)
- .fade(duration: 0.1)
- .aspectRatio(contentMode: .fit)
- .tabItem {
- Text(url.absoluteString)
- }
- .id(url.absoluteString)
- .modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
- .sheet(isPresented: $showShareSheet) {
- ShareSheet(activityItems: [url])
- }
- //.padding(100)
- .scaledToFit()
- .scaleEffect(self.scale * scaleState)
- .offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
- .gesture(SimultaneousGesture(zoomGesture, dragGesture))
- .gesture(doubleTapGesture)
- .modifier(SwipeToDismissModifier(onDismiss: {
- presentationMode.wrappedValue.dismiss()
- }))
-
- }.padding(.bottom, 50) // Ensure carousel appears beneath
+ .ignoresSafeArea()
+ .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
+ .onChange(of: selectedIndex, perform: { _ in
+ showMenu = true
+ })
+ .onTapGesture {
+ showMenu.toggle()
+ }
+ .overlay(
+ VStack {
+ if showMenu {
+ navBarView
+ Spacer()
+
+ if (urls.count > 1) {
+ tabViewIndicator
+ }
}
}
- }
+ .animation(.easeInOut, value: showMenu)
+ .padding(
+ .bottom,
+ UIApplication
+ .shared
+ .connectedScenes
+ .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
+ .first { $0.isKeyWindow }?.safeAreaInsets.bottom
+ )
+ )
}
- .tabViewStyle(PageTabViewStyle())
}
}
@@ -205,13 +276,15 @@ struct ImageCarousel: View {
.foregroundColor(Color.clear)
.overlay {
KFAnimatedImage(url)
- .configure { view in
- view.framePreloadCount = 3
- }
+ .callbackQueue(.dispatch(.global(qos: .background)))
+ .processingQueue(.dispatch(.global(qos: .background)))
.cacheOriginalImage()
.loadDiskFileSynchronously()
.scaleFactor(UIScreen.main.scale)
.fade(duration: 0.1)
+ .configure { view in
+ view.framePreloadCount = 3
+ }
.aspectRatio(contentMode: .fit)
.tabItem {
Text(url.absoluteString)
diff --git a/damus/Modifiers/SwipeToDismiss.swift b/damus/Modifiers/SwipeToDismiss.swift
@@ -8,6 +8,7 @@
import SwiftUI
struct SwipeToDismissModifier: ViewModifier {
+ let minDistance: CGFloat?
var onDismiss: () -> Void
@State private var offset: CGSize = .zero
@@ -16,7 +17,7 @@ struct SwipeToDismissModifier: ViewModifier {
.offset(y: offset.height)
.animation(.interactiveSpring(), value: offset)
.simultaneousGesture(
- DragGesture()
+ DragGesture(minimumDistance: minDistance ?? 10)
.onChanged { gesture in
if gesture.translation.width < 50 {
offset = gesture.translation
diff --git a/damus/Views/ProfileZoomView.swift b/damus/Views/ProfileZoomView.swift
@@ -78,7 +78,7 @@ struct ProfileZoomView: View {
.offset(x: offset.width + offsetState.width, y: offset.height + offsetState.height)
.gesture(SimultaneousGesture(zoomGesture, dragGesture))
.gesture(doubleTapGesture)
- .modifier(SwipeToDismissModifier(onDismiss: {
+ .modifier(SwipeToDismissModifier(minDistance: nil, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))