summaryrefslogtreecommitdiff
path: root/Sora/Views/InteractiveImageView.swift
blob: 7afde7d52e3b0f6fb31075085ba442c3ac007118 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import SwiftUI

struct InteractiveImageView: View {
  let image: Image
  @State private var screenWidth = 0.0
  @State private var screenHeight = 0.0
  @State private var currentScale = 1.0
  @State private var previousScale = 0.0
  @State private var currentOffset: CGSize = .zero
  @State private var previousOffset: CGSize = .zero

  var body: some View {
    GeometryReader { geometry in
      VStack {
        image
          .resizable()
          .scaledToFit()
          .scaleEffect(currentScale)
          .offset(currentOffset)
          .frame(width: screenWidth, height: screenHeight)
          .clipped()
          .gesture(
            MagnifyGesture()
              .onChanged { gesture in
                withAnimation(.interactiveSpring()) {
                  currentScale =
                    previousScale + gesture.magnification - (previousScale == 0 ? 0 : 1)
                }
              }
              .onEnded { _ in
                previousScale = currentScale
              }
              .simultaneously(
                with: DragGesture(minimumDistance: 0)
                  .onChanged { gesture in
                    withAnimation(.interactiveSpring()) {
                      var newOffset: CGSize = .zero
                      let offset = gesture.translation

                      newOffset.width = offset.width + previousOffset.width
                      newOffset.height = offset.height + previousOffset.height

                      currentOffset = clampOffset(offset: newOffset)
                    }
                  }
                  .onEnded { _ in
                    previousOffset = currentOffset
                  }
              )
          )
          .highPriorityGesture(
            TapGesture(count: 2)
              .onEnded {
                withAnimation {
                  currentScale = 1.0
                  previousScale = 0
                  currentOffset = .zero
                  previousOffset = .zero
                }
              }
          )
      }
      .onAppear {
        screenWidth = geometry.size.width
        screenHeight = geometry.size.height
      }
    }
  }

  private func clampOffset(offset: CGSize = .zero) -> CGSize {
    var newOffset = offset

    if currentScale > 1 {
      let maxX = ((screenWidth * currentScale) - screenWidth) / 2
      let maxY = ((screenHeight * currentScale) - screenHeight) / 2

      newOffset.width = min(max(-maxX, newOffset.width), maxX)
      newOffset.height = min(max(-maxY, newOffset.height), maxY)
    } else {
      newOffset = .zero
    }

    return newOffset
  }
}

#Preview {
  InteractiveImageView(image: Image(systemName: "photo"))
}