Last active
March 20, 2017 18:25
-
-
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.
This file contains hidden or 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
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