Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
erikdrobne committed Jan 20, 2022
0 parents commit 803dce3
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
25 changes: 25 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version:5.5

import PackageDescription

let package = Package(
name: "CameraButton",
platforms: [
.iOS(.v10)
],
products: [
.library(
name: "CameraButton",
targets: ["CameraButton"])
],
targets: [
.target(
name: "CameraButton",
dependencies: [],
path: "Sources"
),
.testTarget(
name: "CameraButtonTests",
dependencies: ["CameraButton"])
]
)
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# CameraButton

A simple camera button that can be used for photo and video capturing. It's a subclass of the native `UIButton` with attributes for customization.

## Requirements

iOS 10 or higher

## Instalation

### Swift Package Manager

```Swift
dependencies: [
.package(url: "https://github.com/erikdrobne/CameraButton")
]
```

## Usage

### Import

```Swift
import CameraButton
```

### Initialize

```Swift
let button = CameraButton()
button.delegate = self

view.addSubview(button)

button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 72),
button.heightAnchor.constraint(equalToConstant: 72),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -64)
])
```

### Customize

```Swift
button.borderColor = .red
button.fillColor = (.purple, .orange)
button.progressColor = .green

// Set progress animation duration
button.progressDuration = 5

// Start progress animation
button.start()

// Stop progress animation
button.stop()
```

### Delegate

The `CameraButtonDelegate` requires you to implement the following methods:

```Swift
func didTap(_ button: CameraButton)
func didFinishProgress()
```
194 changes: 194 additions & 0 deletions Sources/CameraButton/CameraButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import Foundation
import UIKit

public protocol CameraButtonDelegate: AnyObject {
func didTap(_ button: CameraButton)
func didFinishProgress()
}

public class CameraButton: UIButton, CAAnimationDelegate {

// MARK: - Private properties

private let borderLayer = CAShapeLayer()
private let progressLayer = CAShapeLayer()
private let shapeLayer = CAShapeLayer()

private (set) public var isRecording = false

private struct Animation {
static let progress = (id: "progress", key: "strokeEnd", index: 0)
static let tap = (id: "tap", key: "transform.scale", index: 1)
}

// MARK: - Public properties

public weak var delegate: CameraButtonDelegate?
public var borderColor = UIColor.white
public var fillColor: (default: UIColor, record: UIColor) = (.white, .white)
public var progressColor = UIColor.red
public var progressDuration: TimeInterval = 5

// MARK: - Initialization

public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}

public required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}

// MARK: - Lifecycle

public override func layoutSubviews() {
super.layoutSubviews()

layer.cornerRadius = min(bounds.width, bounds.height) / 2
setupBorderLayer()
setupProgressLayer()
setupShapeLayer()
}

// MARK: - Public methods

public func start() {
guard !isRecording else {
return
}

isRecording = true
borderLayer.opacity = 0
progressLayer.opacity = 1
shapeLayer.fillColor = fillColor.record.cgColor

UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: ({
self.backgroundColor = self.fillColor.record.withAlphaComponent(0.6)
}), completion: { _ in
self.animateProgress(duration: self.progressDuration)
})
}

public func stop() {
guard isRecording else {
return
}

isRecording = false
progressLayer.opacity = 0
borderLayer.opacity = 1
shapeLayer.fillColor = fillColor.default.cgColor

UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: ({
self.backgroundColor = .clear
}), completion: { _ in
self.clearProgressAnimation()
})
}

// MARK: - Private methods

private func setup() {
clipsToBounds = false
backgroundColor = .clear
addTarget(self, action: #selector(handleTap), for: .touchUpInside)
}

private func setupBorderLayer() {
layer.addSublayer(borderLayer)
borderLayer.strokeColor = borderColor.cgColor
borderLayer.lineWidth = frame.width * 0.05
borderLayer.fillColor = nil
borderLayer.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)

let diameter = frame.width
let rect = CGRect(x: -diameter / 2, y: -diameter / 2, width: diameter, height: diameter)
borderLayer.path = UIBezierPath(ovalIn: rect).cgPath
}

private func setupShapeLayer() {
layer.addSublayer(shapeLayer)
shapeLayer.fillColor = fillColor.default.cgColor
shapeLayer.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)

let diameter = frame.width * 0.87
let rect = CGRect(x: -diameter / 2, y: -diameter / 2, width: diameter, height: diameter)
shapeLayer.path = UIBezierPath(ovalIn: rect).cgPath
}

private func setupProgressLayer() {
layer.addSublayer(progressLayer)
progressLayer.strokeColor = progressColor.cgColor
progressLayer.lineWidth = frame.width * 0.08
progressLayer.opacity = 0
progressLayer.strokeEnd = 0
progressLayer.lineCap = .round
progressLayer.fillColor = nil
progressLayer.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)

let diameter = frame.width
let rect = CGRect(x: -diameter / 2, y: -diameter / 2, width: diameter, height: diameter)
let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: .allCorners,
cornerRadii: CGSize(width: diameter, height: diameter)
)

progressLayer.path = path.cgPath
}

private func animateProgress(duration t: TimeInterval) {
let animation = CABasicAnimation(keyPath: Animation.progress.key)
animation.delegate = self
animation.duration = t
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.setValue(Animation.progress.index, forKey: Animation.progress.id)
progressLayer.strokeEnd = 1.0
progressLayer.add(animation, forKey: Animation.progress.key)
}

private func clearProgressAnimation() {
progressLayer.removeAnimation(forKey: Animation.progress.key)
progressLayer.strokeEnd = 0
progressLayer.opacity = 0
progressLayer.layoutIfNeeded()
}

private func animateTap(duration t: TimeInterval) {
let animation = CABasicAnimation(keyPath: Animation.tap.key)
animation.duration = t
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
animation.toValue = [0.9, 0.9]
animation.autoreverses = true
animation.setValue(Animation.tap.index, forKey: Animation.tap.id)
shapeLayer.add(animation, forKey: Animation.tap.key)
}

@objc private func handleTap(_ sender: CameraButton) {
DispatchQueue.main.async { [weak self] in
UIImpactFeedbackGenerator(style: .light).impactOccurred()
self?.animateTap(duration: 0.15)
self?.delegate?.didTap(sender)
}
}

// MARK: - CAAnimationDelegate

public func animationDidStop(_ animation: CAAnimation, finished flag: Bool) {
guard
flag,
animation.value(forKey: Animation.progress.id) as? Int == Animation.progress.index
else {
return
}

DispatchQueue.main.async { [weak self] in
self?.stop()
self?.delegate?.didFinishProgress()
}
}
}
10 changes: 10 additions & 0 deletions Tests/CameraButtonTests/CameraButtonTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import XCTest
@testable import CameraButton

final class CameraButtonTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}

0 comments on commit 803dce3

Please sign in to comment.