Skip to content

Instantly share code, notes, and snippets.

@cyrilchandelier
Created October 29, 2019 06:34
Show Gist options
  • Save cyrilchandelier/602afdbf23ab02e2e9a77bde7bb2a105 to your computer and use it in GitHub Desktop.
Save cyrilchandelier/602afdbf23ab02e2e9a77bde7bb2a105 to your computer and use it in GitHub Desktop.
import UIKit
@IBDesignable final class MediaProgressBar: UIControl {
// MARK: - Configuration
@IBInspectable var isThumbVisible: Bool = true {
didSet {
thumbView.alpha = isThumbVisible ? 1.0 : 0.0
}
}
@IBInspectable var progress: Float = 0.0 {
didSet {
refreshProgress()
}
}
@IBInspectable var buffer: Float = 0.0 {
didSet {
refreshBuffer()
}
}
// MARK: - Customization
@IBInspectable var trackColor: UIColor = UIColor.white {
didSet {
trackView.backgroundColor = trackColor
}
}
@IBInspectable var trackHeight: CGFloat = 10.0 {
didSet {
trackHeightConstraint.constant = trackHeight
}
}
@IBInspectable var trackRadius: CGFloat = 5.0 {
didSet {
trackView.layer.cornerRadius = trackRadius
}
}
@IBInspectable var bufferColor: UIColor = UIColor.black.withAlphaComponent(0.7) {
didSet {
bufferView.backgroundColor = bufferColor
}
}
@IBInspectable var progressColor: UIColor = UIColor.black {
didSet {
progressView.backgroundColor = progressColor
}
}
@IBInspectable var thumbColor: UIColor = UIColor.black {
didSet {
thumbView.backgroundColor = thumbColor
}
}
@IBInspectable var thumbRadius: CGFloat = 20.0 {
didSet {
thumbHeightConstraint.constant = thumbRadius
thumbView.layer.cornerRadius = thumbRadius / 2.0
}
}
// MARK: - Initializers
init() {
fatalError("Unsupported initializer init(), use init(frame:) instead")
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// View must be the same height as intrinsicContentSize
addConstraint(NSLayoutConstraint(item: self,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: thumbRadius))
// Configure and add track view
trackView.backgroundColor = trackColor
trackView.layer.cornerRadius = trackRadius
addSubview(trackView)
addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[trackView]|",
options: .init(),
metrics: nil,
views: ["trackView": trackView]))
addConstraint(NSLayoutConstraint(item: trackView,
attribute: .centerY,
relatedBy: .equal,
toItem: self,
attribute: .centerY,
multiplier: 1.0,
constant: 0))
trackHeightConstraint = NSLayoutConstraint(item: trackView,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: trackHeight)
addConstraint(trackHeightConstraint)
// Configure and add buffer view
bufferView.backgroundColor = bufferColor
trackView.addSubview(bufferView)
bufferViewWidthConstraint = NSLayoutConstraint(item: bufferView,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: 0.0)
addConstraint(bufferViewWidthConstraint)
addConstraint(NSLayoutConstraint(item: bufferView,
attribute: .height,
relatedBy: .equal,
toItem: trackView,
attribute: .height,
multiplier: 1.0,
constant: 0))
// Configure and add progress view
progressView.backgroundColor = progressColor
trackView.addSubview(progressView)
progressViewWidthConstraint = NSLayoutConstraint(item: progressView,
attribute: .width,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: 0.0)
addConstraint(progressViewWidthConstraint)
addConstraint(NSLayoutConstraint(item: progressView,
attribute: .height,
relatedBy: .equal,
toItem: trackView,
attribute: .height,
multiplier: 1.0,
constant: 0))
// Configure and add thumb view
addSubview(thumbView)
thumbView.alpha = isThumbVisible ? 1.0 : 0.0
thumbView.backgroundColor = thumbColor
thumbView.layer.cornerRadius = thumbRadius / 2.0
addConstraint(NSLayoutConstraint(item: thumbView,
attribute: .centerY,
relatedBy: .equal,
toItem: self,
attribute: .centerY,
multiplier: 1.0,
constant: 0.0))
addConstraint(NSLayoutConstraint(item: thumbView,
attribute: .width,
relatedBy: .equal,
toItem: thumbView,
attribute: .height,
multiplier: 1.0,
constant: 0.0))
thumbHeightConstraint = NSLayoutConstraint(item: thumbView,
attribute: .height,
relatedBy: .equal,
toItem: nil,
attribute: .notAnAttribute,
multiplier: 1.0,
constant: thumbRadius)
addConstraint(thumbHeightConstraint)
addConstraint(NSLayoutConstraint(item: thumbView,
attribute: .centerX,
relatedBy: .equal,
toItem: progressView,
attribute: .trailing,
multiplier: 1.0,
constant: 0.0))
}
// MARK: - UI Components & held constraints
private lazy var trackView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.masksToBounds = true
return view
}()
private lazy var progressView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var bufferView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var thumbView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.masksToBounds = true
return view
}()
private var trackHeightConstraint: NSLayoutConstraint!
private var thumbHeightConstraint: NSLayoutConstraint!
private var progressViewWidthConstraint: NSLayoutConstraint!
private var bufferViewWidthConstraint: NSLayoutConstraint!
// MARK: - Interface refres
private func refreshProgress() {
progressViewWidthConstraint.constant = bounds.width * CGFloat(min(max(progress, 0.0), 1.0))
}
private func refreshBuffer() {
bufferViewWidthConstraint.constant = bounds.width * CGFloat(min(max(buffer, 0.0), 1.0))
}
// MARK: - Gesture management
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
sendActions(for: .touchDown)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else {
return
}
let position = touch.location(in: self)
progress = min(max(Float(position.x / bounds.width), 0.0), 1.0)
sendActions(for: .valueChanged)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
sendActions(for: .touchUpInside) // Consider this as an ended event instead
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
sendActions(for: .touchUpInside)
}
override func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
let expandedBounds = bounds.insetBy(dx: min(bounds.width - 44.0, 0), dy: min(bounds.height - 44.0, 0))
expandedBounds.contains(point)
return bounds.contains(point)
}
// MARK: - Layout
override func layoutSubviews() {
super.layoutSubviews()
refreshProgress()
refreshBuffer()
}
override var intrinsicContentSize: CGSize {
let desiredHeight = max(trackHeight, thumbRadius)
return CGSize(width: UIView.noIntrinsicMetric, height: desiredHeight)
}
// MARK: - Interface builder
#if INTERFACE_BUILDER
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
commonInit()
}
#endif
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment