Skip to content

Instantly share code, notes, and snippets.

@JasonCanCode
Last active March 20, 2017 18:25
Show Gist options
  • Save JasonCanCode/a3d2283b965702197f688681d3bf8526 to your computer and use it in GitHub Desktop.
Save JasonCanCode/a3d2283b965702197f688681d3bf8526 to your computer and use it in GitHub Desktop.
Overview slides left and/or right to reveal actions. Sliding past a threshold will trigger a set action.
import UIKit
// NOTE: percentageThreshold is shared across all instances of SwipeControl
private var percentageThreshold: Int = 75
private enum Direction {
case left, right, none
}
// MARK: Public Classes
public typealias ActionBlock = (Void) -> Void
public enum SCViewType {
case overView, leftView, rightView
}
public class SCView: UIView, SCProtocol {
public var leftImage: UIImageView?
public var rightImage: UIImageView?
public var label: UILabel?
public var action: ActionBlock?
public var spinner: UIActivityIndicatorView?
fileprivate var font = UIFont.systemFont(ofSize: 18, weight: UIFontWeightRegular) {
didSet {
label?.font = font
}
}
public init(frame: CGRect = CGRect.zero, type: SCViewType, leftImageName: String? = nil, rightImageName: String? = nil, labelText: String? = nil, action: ActionBlock? = nil) {
super.init(frame: frame)
self.action = action
var loaderOriginX: CGFloat?
let rightOriginX = (width - imageWidth - margin)
let leftOriginX = margin
let alignment: NSTextAlignment
switch type {
case .overView:
alignment = .center
backgroundColor = .blue
rightImage = generateImageView(imageName: rightImageName, originX: rightOriginX)
addToView(rightImage)
leftImage = generateImageView(imageName: leftImageName, originX: leftOriginX)
addToView(leftImage)
case .leftView:
alignment = .left
loaderOriginX = rightOriginX
backgroundColor = .green
leftImage = generateImageView(imageName: leftImageName, originX: leftOriginX)
addToView(leftImage)
case .rightView:
alignment = .right
loaderOriginX = leftOriginX
backgroundColor = .red
rightImage = generateImageView(imageName: rightImageName, originX: rightOriginX)
addToView(rightImage)
}
label = generateLabel(labelText: labelText, labelTextColor: UIColor.white, buffer: imageBuffer, alignment: alignment)
addToView(label)
if let x = loaderOriginX {
spinner = UIActivityIndicatorView(frame: CGRect(x: x, y: 0.0, width: imageWidth, height: height))
spinner!.activityIndicatorViewStyle = .gray
spinner!.startAnimating()
addSubview(spinner!)
}
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class SwipeControl: UIView {
public var overView: UIView!
public var swipeRightView: UIView?
public var swipeLeftView: UIView?
private var placeholderView: UIView?
private var scrollView: SwipeControllerScrollView!
private var underView: UIView!
private let controller = SwipeControllerViewController()
private var multiDirectional: Bool {
return swipeRightView != nil && swipeLeftView != nil
}
private var swipeRightAction: ActionBlock = { _ in
log.warning("!!!SWIPE RIGHT ACTION NOT CONFIGURED!!!")
assertionFailure()
}
private var swipeLefttAction: ActionBlock = { _ in
log.warning("!!!SWIPE LEFT ACTION NOT CONFIGURED!!!")
assertionFailure()
}
public init(frame: CGRect, overView: UIView, leftsideUnderView: UIView?, rightSideUnderView: UIView?) {
super.init(frame: frame)
configure(overView: overView, leftSideUnderView: leftsideUnderView, rightSideUnderView: rightSideUnderView)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
placeholderView = SCView(frame: bounds, type: .overView, labelText: "YOU NEED TO CALL CONFIG!!!")
placeholderView?.backgroundColor = .gray
addSubview(placeholderView!)
}
public func configure(overView: UIView, leftSideUnderView: UIView?, rightSideUnderView: UIView?) {
placeholderView?.removeFromSuperview()
placeholderView = nil
swipeRightView = leftSideUnderView
swipeRightView?.frame = bounds
swipeLeftView = rightSideUnderView
swipeLeftView?.frame = bounds
var restingOffset = CGPoint(x: UIScreen.main.bounds.width, y: 0.0)
self.overView = overView
overView.frame = CGRect(x: restingOffset.x, y: 0, width: UIScreen.main.bounds.width, height: bounds.size.height)
extractAction(view: swipeLeftView, type: .rightView)
extractAction(view: swipeRightView, type: .leftView)
if leftSideUnderView == nil && rightSideUnderView != nil {
restingOffset = CGPoint.zero
}
underView = UIView(frame: bounds)
underView.backgroundColor = UIColor.blue
addSubview(underView)
scrollView = SwipeControllerScrollView(frame: bounds, directionUpdate: setUnderView, swipeAction: performAction, overView: self.overView, restingOffset: restingOffset, isMultidirectional: swipeRightView != nil && swipeLeftView != nil)
scrollView.delegate = controller
addSubview(scrollView)
}
public func setPercentageThreshold(_ threshold: Int) {
percentageThreshold = threshold
}
public func setLabelsFont(_ font: UIFont) {
var views = [SCView]()
views.append(overView)
views.append(swipeLeftView)
views.append(swipeRightView)
for view in views {
view.font = font
}
}
public func addAction(type: SCViewType, action: @escaping ActionBlock) {
switch type {
case .leftView:
swipeRightAction = action
case .rightView:
swipeLefttAction = action
case .overView:
break
}
}
public func resetControl() {
scrollView.resetOverView()
}
private func extractAction(view: UIView?, type: SCViewType) {
guard let view = view as? SCView, let action = view.action else { return }
addAction(type: type, action: action)
}
private func setUnderView(direction: Direction) {
swipeLeftView?.removeFromSuperview()
swipeRightView?.removeFromSuperview()
switch direction {
case .left:
if let swipeLeftView = swipeLeftView {
underView.addSubview(swipeLeftView)
}
case .right:
if let swipeRightView = swipeRightView {
underView.addSubview(swipeRightView)
}
case .none:
break
}
}
private func performAction(direction: Direction) {
switch direction {
case .left:
swipeLefttAction()
case .right:
swipeRightAction()
case .none:
break
}
}
}
// MARK: Swipe Control Protocols
private protocol SCContainerType {
var height: CGFloat { get }
var width: CGFloat { get }
var margin: CGFloat { get }
}
private protocol SCImageContainerType: SCContainerType {
var imageWidth: CGFloat { get }
var imageBuffer: CGFloat { get }
func generateImageView(imageName: String?, originX: CGFloat) -> UIImageView?
}
private protocol SCLabelContainerType: SCContainerType {
var font: UIFont { get set }
func generateLabel(labelText: String?, labelTextColor: UIColor, buffer: CGFloat, alignment: NSTextAlignment) -> UILabel?
}
private protocol SCProtocol: SCImageContainerType, SCLabelContainerType { }
// MARK: Extensions
private extension SCContainerType where Self: UIView {
var height: CGFloat { return bounds.size.height }
var width: CGFloat { return UIScreen.main.bounds.width }
var margin: CGFloat { return 8.0 }
func addToView(_ view: UIView?) {
if let view = view {
addSubview(view)
}
}
}
private extension SCImageContainerType where Self: UIView {
var imageWidth: CGFloat { return 30.0 }
var imageBuffer: CGFloat { return imageWidth + (2 * margin) }
func generateImageView(imageName: String?, originX: CGFloat) -> UIImageView? {
guard let name = imageName else { return nil }
let imageView = UIImageView(frame: CGRect(x: originX, y: 0, width: imageWidth, height: bounds.size.height))
imageView.image = UIImage(named: name)?.withRenderingMode(.alwaysTemplate)
imageView.tintColor = .white
imageView.contentMode = .center
return imageView
}
}
private extension SCLabelContainerType where Self: UIView {
func generateLabel(labelText: String?, labelTextColor: UIColor, buffer: CGFloat, alignment: NSTextAlignment) -> UILabel? {
guard let text = labelText else { return nil }
let label = UILabel(frame: CGRect(x: buffer, y: margin, width: width - (buffer * 2), height: height - (margin * 2)))
label.text = text
label.textAlignment = alignment
label.numberOfLines = 0
label.font = font
label.backgroundColor = .clear
label.textColor = labelTextColor
return label
}
}
private extension Array {
mutating func append(_ newElement: AnyObject?) {
guard let obj = newElement as? Element else { return }
self.append(obj)
}
}
// MARK: Private Classes
private typealias DirectionBlock = (Direction) -> Void
private class SwipeControllerScrollView: UIScrollView {
let directionUpdate: (Direction) -> Void
let swipeAction: (Direction) -> Void
var overView: UIView!
var restingOffset: CGPoint
var direction = Direction.none {
willSet {
if newValue != direction {
directionUpdate(newValue)
}
}
}
init(frame: CGRect, directionUpdate: @escaping DirectionBlock, swipeAction: @escaping DirectionBlock, overView: UIView, restingOffset: CGPoint, isMultidirectional: Bool = false) {
self.directionUpdate = directionUpdate
self.swipeAction = swipeAction
self.restingOffset = restingOffset
super.init(frame: frame)
let totalViews: CGFloat = isMultidirectional ? 3 : 2
backgroundColor = UIColor.clear
contentSize = CGSize(width: UIScreen.main.bounds.width * totalViews, height: frame.height)
autoresizingMask = .flexibleWidth
contentOffset = restingOffset
showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false
bounces = false
self.overView = overView
addSubview(self.overView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func fadeOverView(alpha: CGFloat) {
UIView.animate(withDuration: 0.5, animations: {
self.overView.alpha = alpha
})
}
func swipeFinished() {
if reachedThreshold(offsetX: contentOffset.x) {
swipeAction(direction)
}
updateOverView()
}
func updateOverView() {
if reachedThreshold(offsetX: contentOffset.x) {
hideOverView()
} else {
resetOverView()
}
}
func hideOverView() {
let farRightOffset = CGPoint(x: UIScreen.main.bounds.width + restingOffset.x, y: 0.0)
let actionOffSet = direction == .left ? farRightOffset : CGPoint.zero
fadeOverView(alpha: 0.0)
setContentOffset(actionOffSet, animated: true)
}
func resetOverView() {
fadeOverView(alpha: 1.0)
setContentOffset(restingOffset, animated: true)
}
func reachedThreshold(offsetX: CGFloat) -> Bool {
let difference = max(contentOffset.x - restingOffset.x, restingOffset.x - contentOffset.x)
let percentDifference = Int((difference / frame.width) * 100)
return percentDifference >= percentageThreshold
}
func updateDirection() {
if contentOffset.x > restingOffset.x {
direction = .left
} else if contentOffset.x < restingOffset.x {
direction = .right
} else {
direction = .none
}
}
}
private class SwipeControllerViewController: UIViewController, UIScrollViewDelegate {
@objc func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let scrollView = scrollView as? SwipeControllerScrollView else {
return
}
scrollView.updateDirection()
}
@objc func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
guard let scrollView = scrollView as? SwipeControllerScrollView else {
return
}
scrollView.swipeFinished()
}
@objc func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
guard let scrollView = scrollView as? SwipeControllerScrollView else {
return
}
scrollView.updateOverView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment