-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
David Jonsén
committed
Aug 31, 2018
1 parent
6b8c180
commit 5098ec4
Showing
50 changed files
with
2,872 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# | ||
# Be sure to run `pod lib lint DJCaptureButton.podspec' to ensure this is a | ||
# valid spec before submitting. | ||
# | ||
# Any lines starting with a # are optional, but their use is encouraged | ||
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html | ||
# | ||
|
||
Pod::Spec.new do |s| | ||
s.name = 'DJCaptureButton' | ||
s.version = '0.1.0' | ||
s.summary = 'Camera styled capture button with 3D touch' | ||
|
||
# This description is used to generate tags and improve search results. | ||
# * Think: What does it do? Why did you write it? What is the focus? | ||
# * Try to keep it short, snappy and to the point. | ||
# * Write the description between the DESC delimiters below. | ||
# * Finally, don't worry about the indent, CocoaPods strips it! | ||
|
||
s.description = <<-DESC | ||
This is a camera styled capture button. Used for one of my apps and i wanted to share it. It uses tap and 3D Touch to fire the action. | ||
DESC | ||
|
||
s.homepage = 'https://github.com/davnag/DJCaptureButton' | ||
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' | ||
s.license = { :type => 'MIT', :file => 'LICENSE' } | ||
s.author = { 'David Jonsén' => 'dev.jonsen@outlook.com' } | ||
s.source = { :git => 'https://github.com/davnag/DJCaptureButton.git', :tag => s.version.to_s } | ||
# s.social_media_url = 'https://twitter.com/' | ||
|
||
s.swift_version = '4.0' | ||
|
||
s.ios.deployment_target = '10.0' | ||
|
||
s.source_files = 'DJCaptureButton/Classes/**/*' | ||
|
||
end |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
// | ||
// CaptureButton.swift | ||
// CaptureButton | ||
// | ||
// Created by David Jonsén on 2018-08-31. | ||
// Copyright © 2018 David Jonsén. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
@objc | ||
public protocol CaptureButtonDelegate: class { | ||
|
||
func captureButtonDidFire(captureButton: CaptureButton) | ||
} | ||
|
||
@IBDesignable | ||
open class CaptureButton: UIButton { | ||
|
||
// MARK: - IBInspectable | ||
|
||
@IBInspectable public var borderWidth: CGFloat = 4.0 { | ||
didSet { | ||
layer.borderWidth = borderWidth | ||
} | ||
} | ||
|
||
@IBInspectable public var borderColor: UIColor = .white { | ||
didSet { | ||
layer.borderColor = borderColor.cgColor | ||
} | ||
} | ||
|
||
@IBInspectable public var middleCircleOffset: CGFloat = 4.0 { | ||
didSet { | ||
updateMiddleCircle() | ||
} | ||
} | ||
|
||
@IBInspectable public var middleCircleColor: UIColor = .white { | ||
didSet { | ||
middleCircle.fillColor = middleCircleColor.cgColor | ||
} | ||
} | ||
|
||
// MARK: - Public | ||
|
||
public weak var delegate: CaptureButtonDelegate? | ||
|
||
public var generateFeedback: Bool = true | ||
|
||
internal lazy var impactFeedbackGenerator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) | ||
|
||
// MARK: - Internal | ||
|
||
internal var forceTouchGestureRecognizer: ForceTouchGestureRecognizer? | ||
|
||
internal lazy var tapGestureRecognizer: UITapGestureRecognizer = { | ||
let gesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) | ||
gesture.delegate = self | ||
return gesture | ||
}() | ||
|
||
internal lazy var middleCircle: CAShapeLayer = { | ||
let circle = CAShapeLayer() | ||
return circle | ||
}() | ||
|
||
// MARK: - | ||
|
||
public override init(frame: CGRect) { | ||
super.init(frame: frame) | ||
|
||
setupButton() | ||
} | ||
|
||
public required init?(coder aDecoder: NSCoder) { | ||
super.init(coder: aDecoder) | ||
|
||
setupButton() | ||
} | ||
|
||
open override func layoutSubviews() { | ||
super.layoutSubviews() | ||
|
||
let radius = bounds.size.width / 2.0 | ||
layer.cornerRadius = radius | ||
|
||
updateMiddleCircle() | ||
} | ||
|
||
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { | ||
super.traitCollectionDidChange(previousTraitCollection) | ||
|
||
guard forceTouchGestureRecognizer == nil else { | ||
|
||
forceTouchGestureRecognizer?.isEnabled = forceTouchCapabilityAvailable() | ||
return | ||
} | ||
|
||
if forceTouchCapabilityAvailable() { | ||
setupForceTouchGestureRecognizer() | ||
} | ||
} | ||
} | ||
|
||
extension CaptureButton { | ||
|
||
internal func setupButton() { | ||
|
||
setupBorder() | ||
setupMiddleCircle() | ||
setupTapGestureRecognizer() | ||
} | ||
|
||
internal func setupBorder() { | ||
|
||
layer.borderWidth = borderWidth | ||
layer.borderColor = borderColor.cgColor | ||
} | ||
|
||
internal func setupMiddleCircle() { | ||
|
||
middleCircle.fillColor = middleCircleColor.cgColor | ||
middleCircle.frame = bounds | ||
|
||
layer.addSublayer(middleCircle) | ||
} | ||
|
||
internal func setupTapGestureRecognizer() { | ||
|
||
addGestureRecognizer(tapGestureRecognizer) | ||
} | ||
|
||
internal func setupForceTouchGestureRecognizer() { | ||
|
||
let forceGesture = ForceTouchGestureRecognizer(target: self, action: #selector(handleForce)) | ||
forceGesture.delegate = self | ||
addGestureRecognizer(forceGesture) | ||
forceTouchGestureRecognizer = forceGesture | ||
} | ||
|
||
internal func updateMiddleCircle() { | ||
|
||
let offset = borderWidth + middleCircleOffset | ||
|
||
middleCircle.path = UIBezierPath(ovalIn: CGRect(x: offset, y: offset, width: bounds.width - (offset * 2), height: bounds.height - (offset * 2))).cgPath | ||
} | ||
} | ||
|
||
extension CaptureButton { | ||
|
||
@objc | ||
internal func handleTap(gesture: ForceTouchGestureRecognizer) { | ||
|
||
animateTapGesture() | ||
|
||
fireButton() | ||
} | ||
|
||
@objc | ||
internal func handleForce(gesture: ForceTouchGestureRecognizer) { | ||
|
||
switch gesture.state { | ||
|
||
case .ended, .cancelled: | ||
|
||
resetMiddleCircle(delay: 100) | ||
|
||
tapGestureRecognizer.isEnabled = true | ||
|
||
case .changed: | ||
|
||
let scale = 1 - min(gesture.force, 0.2) | ||
|
||
middleCircle.transform = CATransform3DMakeScale(scale, scale, 1.0) | ||
|
||
guard scale <= 0.8 else { | ||
return | ||
} | ||
|
||
tapGestureRecognizer.isEnabled = false | ||
|
||
gesture.isEnabled = false | ||
gesture.isEnabled = true | ||
|
||
fireButton() | ||
|
||
default: | ||
break | ||
} | ||
} | ||
} | ||
|
||
extension CaptureButton { | ||
|
||
internal func resetMiddleCircle(delay milliseconds: Int = 0) { | ||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(milliseconds)) { | ||
self.middleCircle.transform = CATransform3DMakeScale(1.0, 1.0, 1.0) | ||
} | ||
} | ||
|
||
internal func fireButton() { | ||
|
||
if generateFeedback { | ||
impactFeedbackGenerator.impactOccurred() | ||
} | ||
|
||
delegate?.captureButtonDidFire(captureButton: self) | ||
} | ||
} | ||
|
||
extension CaptureButton { | ||
|
||
public func animateTapGesture() { | ||
|
||
middleCircle.transform = CATransform3DMakeScale(0.9, 0.9, 1.0) | ||
|
||
resetMiddleCircle(delay: 100) | ||
} | ||
} | ||
|
||
extension CaptureButton: UIGestureRecognizerDelegate { | ||
|
||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { | ||
|
||
return true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// | ||
// ForceTouchGestureRecognizer.swift | ||
// CaptureButton | ||
// | ||
// Created by David Jonsén on 2018-08-31. | ||
// Copyright © 2018 David Jonsén. All rights reserved. | ||
// | ||
|
||
import UIKit.UIGestureRecognizerSubclass | ||
|
||
final class ForceTouchGestureRecognizer: UIGestureRecognizer { | ||
|
||
private(set) var force: CGFloat = 0.0 | ||
|
||
convenience init() { | ||
self.init(target: nil, action: nil) | ||
|
||
setupGesture() | ||
} | ||
|
||
override init(target: Any?, action: Selector?) { | ||
super.init(target: target, action: action) | ||
|
||
setupGesture() | ||
} | ||
|
||
private func setupGesture() { | ||
|
||
cancelsTouchesInView = false | ||
} | ||
|
||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { | ||
super.touchesBegan(touches, with: event) | ||
|
||
updateForce(.began, touches: touches) | ||
} | ||
|
||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) { | ||
super.touchesMoved(touches, with: event) | ||
|
||
updateForce(.changed, touches: touches) | ||
} | ||
|
||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) { | ||
super.touchesEnded(touches, with: event) | ||
|
||
updateForce(.ended, touches: touches) | ||
} | ||
|
||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) { | ||
super.touchesCancelled(touches, with: event) | ||
|
||
updateForce(.cancelled, touches: touches) | ||
} | ||
|
||
private func updateForce(_ state: UIGestureRecognizerState, touches: Set<UITouch>) { | ||
|
||
guard let firstTouch = touches.first else { | ||
return | ||
} | ||
|
||
force = firstTouch.force / firstTouch.maximumPossibleForce | ||
|
||
self.state = state | ||
} | ||
|
||
public override func reset() { | ||
super.reset() | ||
|
||
force = 0.0 | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
DJCaptureButton/Classes/UIView+forceTouchCapabilityAvailable.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// | ||
// UIView+forceTouchCapabilityAvailable.swift | ||
// CaptureButton | ||
// | ||
// Created by David Jonsén on 2018-08-31. | ||
// Copyright © 2018 David Jonsén. All rights reserved. | ||
// | ||
|
||
import UIKit | ||
|
||
extension UIView { | ||
|
||
func forceTouchCapabilityAvailable() -> Bool { | ||
|
||
return traitCollection.forceTouchCapability == .available | ||
} | ||
} |
Oops, something went wrong.