summaryrefslogtreecommitdiff
path: root/Sora/Views/InteractiveImageView.swift
blob: dfee98f85a2390bf518dfd991e84708880b6a718 (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
90
91
92
93
94
95
96
97
98
99
100
import SwiftUI

struct InteractiveImageView<ContextMenuContent: View>: View {
  let image: Image
  let contextMenu: ContextMenuContent
  @State private var screenWidth = 0.0
  @State private var screenHeight = 0.0
  @State private var currentScale = 1.0
  @State private var previousScale = 1.0
  @State private var currentOffset: CGSize = .zero
  @State private var previousOffset: CGSize = .zero
  @State private var zoomAnchor: UnitPoint = .center

  var body: some View {
    image
      .resizable()
      .scaledToFit()
      .scaleEffect(currentScale, anchor: zoomAnchor)
      .offset(currentOffset)
      .contextMenu { contextMenu }
      .frame(maxWidth: .infinity, maxHeight: .infinity)
      .clipped()
      .background(
        GeometryReader { geometry in
          Color.clear
            .onAppear {
              screenWidth = geometry.size.width
              screenHeight = geometry.size.height
            }
        }
      )
      .gesture(
        MagnifyGesture()
          .onChanged { gesture in
            withAnimation(.interactiveSpring()) {
              if previousScale == 1 {
                zoomAnchor = gesture.startAnchor
              }

              currentScale = max(previousScale * gesture.magnification, 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 = currentScale == 1 ? 2 : 1
              previousScale = currentScale
              currentOffset = .zero
              previousOffset = .zero
              zoomAnchor = .center
            }
          }
      )
  }

  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"),
    contextMenu: EmptyView()
  )
}