Skip to content

Instantly share code, notes, and snippets.

@ElegyD
Forked from JULI-ya/SeparatorView.swift
Last active September 12, 2019 23:15
Show Gist options
  • Save ElegyD/c5af8892de04fa8a33e26fb919972d6a to your computer and use it in GitHub Desktop.
Save ElegyD/c5af8892de04fa8a33e26fb919972d6a to your computer and use it in GitHub Desktop.
SeparatorView in Swift 4 with drag indicator
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)
}
}
@Coder-ACJHP
Copy link

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 :

Property 'self.primaryView' not initialized at super.init call

@momopry
Copy link

momopry commented Sep 12, 2019

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