Impressia/Vernissage/Widgets/ImagesViewer.swift

205 lines
7.6 KiB
Swift
Raw Normal View History

2023-01-13 21:11:30 +01:00
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct ImagesViewer: View {
@Environment(\.dismiss) private var dismiss
2023-02-20 14:25:46 +01:00
private var statusViewModel: StatusModel
private var selectedAttachmentId: String = String.empty()
private let image: Image?
2023-02-20 10:57:16 +01:00
private let closeDragDistance = 100.0
2023-01-14 20:45:49 +01:00
// Opacity usied during close dialog animation.
@State private var opacity = 1.0
2023-01-14 11:57:28 +01:00
2023-01-13 21:11:30 +01:00
// Zoom.
2023-01-14 11:57:28 +01:00
@State private var zoomScale = 1.0
2023-01-13 21:11:30 +01:00
// Magnification.
2023-02-20 10:57:16 +01:00
@State private var currentMagnification = 0.0
@State private var finalMagnification = 1.0
// Rotation.
@State private var rotationAngle = Angle.zero
2023-01-13 21:11:30 +01:00
// Draging.
@State private var currentOffset = CGSize.zero
2023-01-14 11:57:28 +01:00
@State private var accumulatedOffset = CGSize.zero
2023-01-13 21:11:30 +01:00
2023-02-20 14:25:46 +01:00
init(statusViewModel: StatusModel, selectedAttachmentId: String) {
self.statusViewModel = statusViewModel
self.selectedAttachmentId = selectedAttachmentId
if let attachment = statusViewModel.mediaAttachments.first(where: { $0.id == selectedAttachmentId }),
2023-02-20 10:57:16 +01:00
let data = attachment.data,
2023-02-20 14:25:46 +01:00
let uiImage = UIImage(data: data) {
self.image = Image(uiImage: uiImage)
} else {
self.image = nil
}
}
var body: some View {
if let image = self.image {
image
2023-02-20 10:57:16 +01:00
.resizable()
.aspectRatio(contentMode: .fit)
2023-02-20 14:25:46 +01:00
.tag(selectedAttachmentId)
2023-02-20 10:57:16 +01:00
.offset(currentOffset)
.rotationEffect(rotationAngle)
.scaleEffect(finalMagnification + currentMagnification)
.opacity(self.opacity)
.gesture(dragGesture)
.gesture(magnificationGesture)
.gesture(doubleTapGesture)
.gesture(tapGesture)
2023-01-13 21:11:30 +01:00
}
}
private func close() {
2023-01-22 21:44:07 +01:00
withoutAnimation {
dismiss()
}
2023-01-13 21:11:30 +01:00
}
2023-02-20 10:57:16 +01:00
@MainActor
2023-01-13 21:11:30 +01:00
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { amount in
2023-02-20 11:55:38 +01:00
currentMagnification = (amount - 1) * self.finalMagnification
2023-01-13 21:11:30 +01:00
}
.onEnded { amount in
2023-02-20 10:57:16 +01:00
let finalMagnification = finalMagnification + currentMagnification
self.revertToPrecalculatedMagnification(magnification: finalMagnification)
2023-01-13 21:11:30 +01:00
}
}
var doubleTapGesture: some Gesture {
TapGesture(count: 2)
.onEnded { _ in
2023-01-14 11:57:28 +01:00
withAnimation {
2023-02-20 10:57:16 +01:00
if self.finalMagnification == 1.0 {
currentOffset = CGSize.zero
accumulatedOffset = CGSize.zero
currentMagnification = 0
finalMagnification = 2.0
} else {
currentOffset = CGSize.zero
accumulatedOffset = CGSize.zero
currentMagnification = 0
finalMagnification = 1.0
}
2023-01-14 11:57:28 +01:00
}
2023-01-13 21:11:30 +01:00
}
}
2023-02-20 10:57:16 +01:00
@MainActor
2023-01-13 21:11:30 +01:00
var dragGesture: some Gesture {
2023-02-20 10:57:16 +01:00
DragGesture()
2023-01-13 21:11:30 +01:00
.onChanged { amount in
2023-02-20 10:57:16 +01:00
// Opacity and rotation is working only when we have small image size.
if self.finalMagnification == 1.0 {
// We can move image whatever we want.
self.currentOffset = CGSize(width: amount.translation.width + self.accumulatedOffset.width,
height: amount.translation.height + self.accumulatedOffset.height)
2023-02-20 14:25:46 +01:00
2023-02-20 10:57:16 +01:00
// Changing opacity when we want to close.
let pictureOpacity = (self.closeDragDistance - self.currentOffset.height) / self.closeDragDistance
self.opacity = pictureOpacity >= 0 ? pictureOpacity : 0
// Changing angle.
self.rotationAngle = Angle(degrees: Double(self.currentOffset.width / 30))
} else {
2023-02-20 11:55:38 +01:00
// Bigger images we can move only horizontally (we have to include magnifications).
let offsetWidth = (amount.translation.width / self.finalMagnification) + self.accumulatedOffset.width
self.currentOffset = CGSize(width: offsetWidth, height: 0)
2023-02-20 10:57:16 +01:00
}
2023-01-13 21:11:30 +01:00
} .onEnded { amount in
2023-02-20 12:23:20 +01:00
self.accumulatedOffset = CGSize(width: (amount.translation.width / self.finalMagnification) + self.accumulatedOffset.width,
height: (amount.translation.height / self.finalMagnification) + self.accumulatedOffset.height)
2023-01-14 20:45:49 +01:00
2023-02-20 10:57:16 +01:00
// Animations only for small images sizes,
if self.finalMagnification == 1.0 {
if self.accumulatedOffset.height < closeDragDistance {
// Revert back image offset.
2023-02-20 14:25:46 +01:00
withAnimation {
2023-02-20 10:57:16 +01:00
self.currentOffset = CGSize.zero
self.accumulatedOffset = CGSize.zero
self.opacity = 1.0
}
} else {
// Close the screen.
2023-02-20 14:25:46 +01:00
withAnimation {
2023-02-20 10:57:16 +01:00
self.currentOffset = amount.predictedEndTranslation
self.accumulatedOffset = CGSize.zero
self.opacity = 1.0
}
self.close()
2023-01-14 20:45:49 +01:00
}
} else {
2023-02-20 10:57:16 +01:00
self.moveToEdge()
2023-01-14 20:45:49 +01:00
}
2023-02-20 10:57:16 +01:00
self.rotationAngle = Angle.zero
2023-01-13 21:11:30 +01:00
}
}
var tapGesture: some Gesture {
TapGesture().onEnded({ _ in
self.close()
})
}
2023-01-14 20:45:49 +01:00
2023-02-20 10:57:16 +01:00
@MainActor
2023-01-14 20:45:49 +01:00
private func revertToPrecalculatedMagnification(magnification: Double) {
if magnification < 1.0 {
// When image is small we are returning to starting point.
2023-02-20 14:25:46 +01:00
withAnimation {
2023-02-20 10:57:16 +01:00
finalMagnification = 1.0
currentMagnification = 0
2023-01-14 20:45:49 +01:00
// Also we have to move image to orginal position.
currentOffset = CGSize.zero
}
2023-02-20 10:57:16 +01:00
HapticService.shared.fireHaptic(of: .animation)
} else if magnification > 3.0 {
2023-01-14 20:45:49 +01:00
// When image is magnified to much we are rturning to 1.5 maginification.
2023-02-20 14:25:46 +01:00
withAnimation {
2023-02-20 10:57:16 +01:00
finalMagnification = 3.0
currentMagnification = 0
2023-01-14 20:45:49 +01:00
}
2023-02-20 10:57:16 +01:00
HapticService.shared.fireHaptic(of: .animation)
2023-01-14 20:45:49 +01:00
} else {
2023-02-20 10:57:16 +01:00
finalMagnification = magnification
currentMagnification = 0
self.moveToEdge()
2023-01-14 20:45:49 +01:00
}
}
2023-02-20 10:57:16 +01:00
@MainActor
private func moveToEdge() {
let maxEdgeDistance = ((UIScreen.main.bounds.width * self.finalMagnification) - UIScreen.main.bounds.width) / (2 * self.finalMagnification)
if self.currentOffset.width > maxEdgeDistance {
self.currentOffset = CGSize(width: maxEdgeDistance, height: 0)
self.accumulatedOffset = self.currentOffset
HapticService.shared.fireHaptic(of: .animation)
} else if self.currentOffset.width < -maxEdgeDistance {
self.currentOffset = CGSize(width: -maxEdgeDistance, height: 0)
self.accumulatedOffset = self.currentOffset
HapticService.shared.fireHaptic(of: .animation)
2023-01-14 20:45:49 +01:00
}
}
2023-01-13 21:11:30 +01:00
}