Skip to content

Commit

Permalink
First commit of DJCaptureButton
Browse files Browse the repository at this point in the history
  • Loading branch information
David Jonsén committed Aug 31, 2018
1 parent 6b8c180 commit 5098ec4
Show file tree
Hide file tree
Showing 50 changed files with 2,872 additions and 2 deletions.
37 changes: 37 additions & 0 deletions DJCaptureButton.podspec
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 added DJCaptureButton/Assets/.gitkeep
Empty file.
Empty file.
230 changes: 230 additions & 0 deletions DJCaptureButton/Classes/CaptureButton.swift
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
}
}
72 changes: 72 additions & 0 deletions DJCaptureButton/Classes/ForceTouchGestureRecognizer.swift
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 DJCaptureButton/Classes/UIView+forceTouchCapabilityAvailable.swift
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
}
}
Loading

0 comments on commit 5098ec4

Please sign in to comment.