From 9582bd59c754a63182cb1ada4db43c9dbb8cbe20 Mon Sep 17 00:00:00 2001
From: noppefoxwolf <noppelabs@gmail.com>
Date: Thu, 30 Jun 2022 22:33:24 +0900
Subject: [PATCH] Add continue pan color pick.

---
 Example.swiftpm/App.swift                     |  5 +--
 Example.swiftpm/ContentView.swift             | 32 ++++++++++++++-
 Example.swiftpm/Package.resolved              |  4 +-
 Package.swift                                 |  2 +-
 .../GoOutPanGestureRecognizer.swift           | 21 ++++++++++
 .../ScopeColorPicker/ScopeColorPicker.swift   |  7 +++-
 .../ScopeColorPickerWindow.swift              | 40 +++++++++++++++----
 .../SliderColorPicker/ColorSlider.swift       |  8 ++--
 8 files changed, 98 insertions(+), 21 deletions(-)
 create mode 100644 Sources/ColorPicker/Feature/ScopeColorPicker/Components/GoOutPanGestureRecognizer.swift

diff --git a/Example.swiftpm/App.swift b/Example.swiftpm/App.swift
index a881c64..c2c5057 100644
--- a/Example.swiftpm/App.swift
+++ b/Example.swiftpm/App.swift
@@ -5,10 +5,7 @@ struct App: SwiftUI.App {
     
     var body: some Scene {
         WindowGroup {
-            HStack {
-                ContentView().ignoresSafeArea()
-                LinearGradient(colors: [.white, .black], startPoint: .top, endPoint: .bottom)
-            }
+            ContentView()
         }
     }
 }
diff --git a/Example.swiftpm/ContentView.swift b/Example.swiftpm/ContentView.swift
index 38818d2..7fbc072 100644
--- a/Example.swiftpm/ContentView.swift
+++ b/Example.swiftpm/ContentView.swift
@@ -1,6 +1,7 @@
 import SwiftUI
 import ColorPicker
 import SnapKit
+import Combine
 
 struct ContentView: UIViewControllerRepresentable {
     func makeUIViewController(context: Context) -> some UIViewController {
@@ -12,6 +13,8 @@ struct ContentView: UIViewControllerRepresentable {
 }
 
 class ContentViewController: UIViewController {
+    var cancellables: Set<AnyCancellable> = []
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         
@@ -28,10 +31,37 @@ class ContentViewController: UIViewController {
         }))
         let imageView = UIImageView(image: UIImage(named: "image"))
         imageView.contentMode = .scaleAspectFit
+        let eyeDropperButton = UIButton(configuration: .filled())
+        eyeDropperButton.configuration?.image = UIImage(systemName: "eyedropper")
+        eyeDropperButton.configuration?.title = "Tap or drag here"
+        eyeDropperButton.addAction(UIAction { [unowned self] _ in
+            let picker = ScopeColorPicker(windowScene: self.view.window!.windowScene!)
+            Task {
+                let color = await picker.pickColor()
+                print(color)
+            }
+        }, for: .primaryActionTriggered)
+        let goOut = GoOutPanGestureRecognizer()
+        goOut
+            .publisher(for: \.state)
+            .filter({ $0 == .began })
+            .sink { [unowned self] state in
+                print("GO")
+                let picker = ScopeColorPicker(
+                    windowScene: self.view.window!.windowScene!,
+                    panGestureRecognizer: goOut
+                )
+                Task {
+                    let color = await picker.pickColor()
+                    print(color)
+                }
+            }.store(in: &cancellables)
+        eyeDropperButton.addGestureRecognizer(goOut)
         let stackView = UIStackView(arrangedSubviews: [
             imageView,
             colorPickerButton,
-            uiColorPickerButton
+            uiColorPickerButton,
+            eyeDropperButton
         ])
         stackView.axis = .vertical
         view.addSubview(stackView)
diff --git a/Example.swiftpm/Package.resolved b/Example.swiftpm/Package.resolved
index e7ecf86..81101ad 100644
--- a/Example.swiftpm/Package.resolved
+++ b/Example.swiftpm/Package.resolved
@@ -6,8 +6,8 @@
         "repositoryURL": "https://github.com/SnapKit/SnapKit",
         "state": {
           "branch": null,
-          "revision": "d458564516e5676af9c70b4f4b2a9178294f1bc6",
-          "version": "5.0.1"
+          "revision": "f222cbdf325885926566172f6f5f06af95473158",
+          "version": "5.6.0"
         }
       }
     ]
diff --git a/Package.swift b/Package.swift
index 327ef93..a5aa8b3 100644
--- a/Package.swift
+++ b/Package.swift
@@ -14,7 +14,7 @@ let package = Package(
         ),
     ],
     dependencies: [
-        .package(url: "https://github.com/SnapKit/SnapKit", from: "5.0.1"),
+        .package(url: "https://github.com/SnapKit/SnapKit", from: "5.6.0"),
     ],
     targets: [
         .target(
diff --git a/Sources/ColorPicker/Feature/ScopeColorPicker/Components/GoOutPanGestureRecognizer.swift b/Sources/ColorPicker/Feature/ScopeColorPicker/Components/GoOutPanGestureRecognizer.swift
new file mode 100644
index 0000000..d331bcd
--- /dev/null
+++ b/Sources/ColorPicker/Feature/ScopeColorPicker/Components/GoOutPanGestureRecognizer.swift
@@ -0,0 +1,21 @@
+import UIKit
+
+/// Viewの外に出た時に初めてbeganになるPanジェスチャ
+public class GoOutPanGestureRecognizer: UIPanGestureRecognizer {
+    
+    public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
+        
+    }
+    
+    public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
+        if state == .possible {
+            let location = touches.first!.location(in: view)
+            if !view!.bounds.contains(location) {
+                super.touchesBegan(touches, with: event)
+                super.touchesMoved(touches, with: event)
+            }
+        } else {
+            super.touchesMoved(touches, with: event)
+        }
+    }
+}
diff --git a/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPicker.swift b/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPicker.swift
index 8bb77b5..aa3b032 100644
--- a/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPicker.swift
+++ b/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPicker.swift
@@ -4,8 +4,11 @@ public class ScopeColorPicker {
     let pickerWindow: ScopeColorPickerWindow
     var continuation: CheckedContinuation<CGColor, Never>? = nil
     
-    public init(windowScene: UIWindowScene) {
-        self.pickerWindow = .init(windowScene: windowScene)
+    public init(
+        windowScene: UIWindowScene,
+        panGestureRecognizer: UIPanGestureRecognizer? = nil
+    ) {
+        self.pickerWindow = .init(windowScene: windowScene, panGestureRecognizer: panGestureRecognizer)
         pickerWindow.delegate = self
         pickerWindow.dataSource = self
     }
diff --git a/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPickerWindow.swift b/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPickerWindow.swift
index 59b5670..637b459 100644
--- a/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPickerWindow.swift
+++ b/Sources/ColorPicker/Feature/ScopeColorPicker/ScopeColorPickerWindow.swift
@@ -15,8 +15,14 @@ class ScopeColorPickerWindow: UIWindow {
     weak var dataSource: ScopeColorPickerDataSource? = nil
     var translationX: Double = 0
     var translationY: Double = 0
+    let isContinuePan: Bool
+    weak var panGestureRecognizer: UIPanGestureRecognizer? = nil
     
-    override init(windowScene: UIWindowScene) {
+    init(
+        windowScene: UIWindowScene,
+        panGestureRecognizer: UIPanGestureRecognizer? = nil
+    ) {
+        isContinuePan = panGestureRecognizer != nil
         super.init(windowScene: windowScene)
         
         addSubview(reticleView)
@@ -26,16 +32,35 @@ class ScopeColorPickerWindow: UIWindow {
             make.size.equalTo(viewSize)
         }
         
-        let panGesture = UIPanGestureRecognizer()
-        panGesture.addTarget(self, action: #selector(onPan(_:)))
-        addGestureRecognizer(panGesture)
+        let panGestureRecognizer = panGestureRecognizer ?? UIPanGestureRecognizer()
+        panGestureRecognizer.addTarget(self, action: #selector(onPan(_:)))
+        if !isContinuePan {
+            addGestureRecognizer(panGestureRecognizer)
+        }
+        self.panGestureRecognizer = panGestureRecognizer
         
         isHidden = false
         
         DispatchQueue.main.async { [weak self] in
-            guard let self = self else { return } 
+            guard let self = self else { return }
             self.updateScopeContent(at: self.center)
         }
+        
+        if isContinuePan {
+            /// continue pan translation
+            let location = panGestureRecognizer.location(in: self)
+            let center = self.center
+            let x = location.x - center.x
+            let y = location.y - center.y
+            let initialTranslation = CGPoint(x: x, y: y)
+            self.translationX = initialTranslation.x
+            self.translationY = initialTranslation.y
+            panGestureRecognizer.setTranslation(initialTranslation, in: panGestureRecognizer.view)
+            reticleView.snp.updateConstraints { make in
+                make.centerX.equalToSuperview().offset(translationX)
+                make.centerY.equalToSuperview().offset(translationY)
+            }
+        }
     }
     
     public required init?(coder: NSCoder) {
@@ -64,6 +89,7 @@ class ScopeColorPickerWindow: UIWindow {
             updateScopeContent(at: reticleView.center)
         case .ended, .failed, .cancelled:
             reticleView.isHidden = true
+            panGestureRecognizer?.removeTarget(self, action: #selector(onPan))
             delegate?.scopePickerDidFinishColorPick(reticleView.color)
         default:
             break
@@ -71,8 +97,8 @@ class ScopeColorPickerWindow: UIWindow {
     }
     
     func updateScopeContent(at location: CGPoint) {
-        reticleView.render { [weak self] context in
-            self?.dataSource?.colors(at: location, context: context)
+        reticleView.render { context in
+            dataSource?.colors(at: location, context: context)
         }
     }
 }
diff --git a/Sources/ColorPicker/Feature/SliderColorPicker/ColorSlider.swift b/Sources/ColorPicker/Feature/SliderColorPicker/ColorSlider.swift
index 19084e7..ca4f843 100644
--- a/Sources/ColorPicker/Feature/SliderColorPicker/ColorSlider.swift
+++ b/Sources/ColorPicker/Feature/SliderColorPicker/ColorSlider.swift
@@ -57,12 +57,12 @@ open class ColorSlider: UIControl {
         }
         trackView.addLayoutGuide(trackableLayoutGuide)
         trackableLayoutGuide.snp.makeConstraints { make in
-            make.left.right.equalToSuperview().inset(34 / 2)
-            make.top.bottom.equalToSuperview()
+            make.horizontalEdges.equalToSuperview().inset(34 / 2)
+            make.verticalEdges.equalToSuperview()
         }
         trackView.addLayoutGuide(trackValueLayoutGuide)
         trackValueLayoutGuide.snp.makeConstraints { make in
-            make.top.bottom.left.equalTo(trackableLayoutGuide)
+            make.verticalEdges.left.equalTo(trackableLayoutGuide)
             make.width.equalTo(trackableLayoutGuide).multipliedBy(0)
         }
         
@@ -101,7 +101,7 @@ open class ColorSlider: UIControl {
         super.setNeedsUpdateConstraints()
 
         trackValueLayoutGuide.snp.remakeConstraints { make in
-            make.top.bottom.left.equalTo(trackableLayoutGuide)
+            make.verticalEdges.left.equalTo(trackableLayoutGuide)
             make.width.equalTo(trackableLayoutGuide).multipliedBy(value)
         }
     }