-
-
Save ElegyD/c5af8892de04fa8a33e26fb919972d6a to your computer and use it in GitHub Desktop.
SeparatorView in Swift 4 with drag indicator
This file contains 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 | |
enum SeparatorType { | |
case horizontal | |
case vertical | |
} | |
let kTotalSize: CGFloat = 18 // the total height of the separator (including parts that are not visible) | |
let kVisibleSize: CGFloat = 8 // the height of the visible portion of the separator | |
let kMargin: CGFloat = (kTotalSize - kVisibleSize) / 2 // the height of the non-visible portions of the separator (i.e. above and below the visible portion) | |
let kMinSize: CGFloat = 32 // the minimum height allowed for views above and below the separator | |
let kIndicatorSize: CGFloat = 36 // size of the indicator in the middle of the separator | |
protocol OnConstraintsUpdateProtocol { | |
func updateConstraintOnBasisOfTouch(touch: UITouch) | |
} | |
class SeparatorView: UIView { | |
var startConstraint: NSLayoutConstraint? // the constraint that dictates the vertical position of the separator | |
var primaryView: UIView // the view above the separator | |
var secondaryView: UIView // the view below the separator | |
// some properties used for handling the touches | |
var oldPosition: CGFloat = 0.0 // the position of the separator before the gesture started | |
var firstTouch: CGPoint? // the position where the drag gesture started | |
var updateListener: OnConstraintsUpdateProtocol? | |
@discardableResult | |
internal static func addSeparatorBetweenViews(separatorType: SeparatorType, primaryView: UIView, secondaryView: UIView, parentView: UIView) -> SeparatorView { | |
var separator: SeparatorView | |
if separatorType == .horizontal { | |
separator = HorizontalSeparatorView(primaryView: primaryView, secondaryView: secondaryView) | |
} else { | |
separator = VerticalSeparatorView(primaryView: primaryView, secondaryView: secondaryView) | |
} | |
separator.setupParentViewConstraints(parentView: parentView) | |
parentView.addSubview(separator) | |
separator.setupSeparatorConstraints() | |
return separator | |
} | |
init(primaryView: UIView, secondaryView: UIView) { | |
self.primaryView = primaryView | |
self.secondaryView = secondaryView | |
super.init(frame: CGRect.zero) | |
self.translatesAutoresizingMaskIntoConstraints = false | |
self.isUserInteractionEnabled = true | |
self.backgroundColor = .clear | |
} | |
func setupParentViewConstraints(parentView: UIView) {} | |
func setupSeparatorConstraints() {} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
self.firstTouch = touches.first?.location(in: self.superview) | |
self.startConstraint!.constant = self.oldPosition | |
self.startConstraint!.isActive = true | |
} | |
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { | |
guard let touch = touches.first, let event = event else { return } | |
// for more responsive UX, use predicted touches, if possible | |
let predictedTouch = event.predictedTouches(for: touch)?.last | |
if predictedTouch != nil { | |
updateListener?.updateConstraintOnBasisOfTouch(touch: predictedTouch!) | |
return | |
} | |
// if no predicted touch found, just use the touch provided | |
updateListener?.updateConstraintOnBasisOfTouch(touch: touch) | |
} | |
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { | |
guard let touch = touches.first else { return } | |
updateListener?.updateConstraintOnBasisOfTouch(touch: touch) | |
} | |
func drawSeparator(_ rect: CGRect, with color: UIColor) { | |
color.set() | |
let path = UIBezierPath(rect: rect) | |
path.stroke() | |
path.fill() | |
} | |
func drawIndicator(_ rect: CGRect, with color: UIColor) { | |
color.set() | |
let path = UIBezierPath(roundedRect: rect, cornerRadius: 2.5) | |
path.stroke() | |
path.fill() | |
} | |
} | |
class HorizontalSeparatorView: SeparatorView, OnConstraintsUpdateProtocol { | |
override init(primaryView: UIView, secondaryView: UIView) { | |
super.init(primaryView: primaryView, secondaryView: secondaryView) | |
updateListener = self | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func setupParentViewConstraints(parentView: UIView) { | |
parentView.leadingAnchor.constraint(equalTo: primaryView.leadingAnchor).isActive = true | |
parentView.trailingAnchor.constraint(equalTo: primaryView.trailingAnchor).isActive = true | |
parentView.leadingAnchor.constraint(equalTo: secondaryView.leadingAnchor).isActive = true | |
parentView.trailingAnchor.constraint(equalTo: secondaryView.trailingAnchor).isActive = true | |
parentView.topAnchor.constraint(equalTo: primaryView.topAnchor).isActive = true | |
let height = secondaryView.heightAnchor.constraint(equalTo: primaryView.heightAnchor) | |
height.priority = .defaultLow | |
height.isActive = true | |
parentView.bottomAnchor.constraint(equalTo: secondaryView.bottomAnchor).isActive = true | |
} | |
override func setupSeparatorConstraints() { | |
self.heightAnchor.constraint(equalToConstant: kTotalSize).isActive = true | |
self.superview?.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true | |
self.superview?.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true | |
primaryView.bottomAnchor.constraint(equalTo: self.topAnchor, constant: kMargin + kVisibleSize / 2).isActive = true | |
secondaryView.topAnchor.constraint(equalTo: self.bottomAnchor, constant: -(kMargin + kVisibleSize / 2)).isActive = true | |
startConstraint = self.topAnchor.constraint(equalTo: self.superview!.topAnchor, constant: 0) | |
} | |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
self.oldPosition = self.frame.origin.y | |
super.touchesBegan(touches, with: event) | |
} | |
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { | |
super.touchesMoved(touches, with: event) | |
} | |
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { | |
super.touchesEnded(touches, with: event) | |
} | |
func updateConstraintOnBasisOfTouch(touch: UITouch) { | |
// calculate where separator should be moved to | |
var y: CGFloat = self.oldPosition + touch.location(in: self.superview).y - self.firstTouch!.y | |
// make sure the views above and below are not too small | |
y = max(y, self.primaryView.frame.origin.y + kMinSize - kMargin) | |
y = min(y, self.secondaryView.frame.origin.y + self.secondaryView.frame.size.height - (kMargin + kMinSize)) | |
// set constraint | |
self.startConstraint!.constant = y | |
} | |
override func draw(_ rect: CGRect) { | |
let separatorRect = CGRect(x: 0, y: kMargin, width: self.bounds.size.width, height: kVisibleSize) | |
let indicatorRect = CGRect(x: (self.bounds.size.width - kIndicatorSize) / 2, y: kMargin + (kVisibleSize - (kVisibleSize / 4)) / 2, width: kIndicatorSize, height: kVisibleSize / 4) | |
super.drawSeparator(separatorRect, with: .white) | |
super.drawIndicator(indicatorRect, with: .lightGray) | |
} | |
} | |
class VerticalSeparatorView: SeparatorView, OnConstraintsUpdateProtocol { | |
override init(primaryView: UIView, secondaryView: UIView) { | |
super.init(primaryView: primaryView, secondaryView: secondaryView) | |
updateListener = self | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func setupParentViewConstraints(parentView: UIView) { | |
parentView.topAnchor.constraint(equalTo: primaryView.topAnchor).isActive = true | |
parentView.topAnchor.constraint(equalTo: secondaryView.topAnchor).isActive = true | |
parentView.bottomAnchor.constraint(equalTo: primaryView.bottomAnchor).isActive = true | |
parentView.leadingAnchor.constraint(equalTo: secondaryView.leadingAnchor).isActive = true | |
parentView.bottomAnchor.constraint(equalTo: secondaryView.bottomAnchor).isActive = true | |
parentView.leadingAnchor.constraint(equalTo: primaryView.leadingAnchor).isActive = true | |
let width = secondaryView.widthAnchor.constraint(equalTo: primaryView.widthAnchor) | |
width.priority = .defaultLow | |
width.isActive = true | |
parentView.trailingAnchor.constraint(equalTo: secondaryView.trailingAnchor).isActive = true | |
} | |
override func setupSeparatorConstraints() { | |
self.widthAnchor.constraint(equalToConstant: kTotalSize).isActive = true | |
self.superview?.topAnchor.constraint(equalTo: self.topAnchor).isActive = true | |
self.superview?.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true | |
primaryView.trailingAnchor.constraint(equalTo: self.leadingAnchor, constant: kMargin + kVisibleSize / 2).isActive = true | |
secondaryView.leadingAnchor.constraint(equalTo: self.trailingAnchor, constant: -(kMargin + kVisibleSize / 2)).isActive = true | |
startConstraint = self.leadingAnchor.constraint(equalTo: self.superview!.leadingAnchor, constant: 0) | |
} | |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
self.oldPosition = self.frame.origin.x | |
super.touchesBegan(touches, with: event) | |
} | |
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { | |
super.touchesMoved(touches, with: event) | |
} | |
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { | |
super.touchesEnded(touches, with: event) | |
} | |
func updateConstraintOnBasisOfTouch(touch: UITouch) { | |
// calculate where separator should be moved to | |
var x: CGFloat = self.oldPosition + touch.location(in: self.superview).x - self.firstTouch!.x | |
// make sure the views above and below are not too small | |
x = max(x, self.primaryView.frame.origin.x + kMinSize - kMargin) | |
x = min(x, self.secondaryView.frame.origin.x + self.secondaryView.frame.size.width - (kMargin + kMinSize)) | |
// set constraint | |
self.startConstraint!.constant = x | |
} | |
override func draw(_ rect: CGRect) { | |
let separatorRect = CGRect(x: kMargin, y: 0, width: kVisibleSize, height: self.bounds.size.height) | |
let indicatorRect = CGRect(x: kMargin + (kVisibleSize - (kVisibleSize / 4)) / 2, y: (self.bounds.size.height - kIndicatorSize) / 2, width: kVisibleSize / 4, height: kIndicatorSize) | |
super.drawSeparator(separatorRect, with: .white) | |
super.drawIndicator(indicatorRect, with: .lightGray) | |
} | |
} |
Although this code runs, there is nothing showing on the screen. When I pass the properties of a view the screen
remains white. I checked to see what is showing in the debug view hierarchy and it just shows one UIView and the Main UIWindow. There is not separator or indicator. Is there something I am missing?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thread 1: Fatal error: init(coder:) has not been implemented
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
when I try to implement it I face with this message :