238 lines
8.2 KiB
Swift
238 lines
8.2 KiB
Swift
//
|
|
// SwitchToggle.swift
|
|
// PrivadoVPN
|
|
//
|
|
// Created by Juraldinio on 8/10/20.
|
|
// Copyright © 2020 Privado LLC. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import AppKit
|
|
|
|
@IBDesignable
|
|
final class SwitchToggle: NSView {
|
|
|
|
// ================================================================================
|
|
// MARK: Properties
|
|
// ================================================================================
|
|
fileprivate var height: CGFloat = 44
|
|
fileprivate var width: CGFloat { return height + ( height * 0.6 ) }
|
|
|
|
fileprivate var leftConstraint: NSLayoutConstraint?
|
|
fileprivate var heightConstraint: NSLayoutConstraint?
|
|
fileprivate var widthConstraint: NSLayoutConstraint?
|
|
|
|
fileprivate let backVw: NSView = {
|
|
let view = NSView()
|
|
view.wantsLayer = true
|
|
view.layer?.masksToBounds = false
|
|
return view
|
|
}()
|
|
|
|
fileprivate let circle: NSView = {
|
|
let view = NSView()
|
|
|
|
let shadow = NSShadow()
|
|
shadow.shadowColor = NSColor.black.withAlphaComponent(0.4)
|
|
shadow.shadowOffset = CGSize(width: 0, height: -2)
|
|
shadow.shadowBlurRadius = 2
|
|
|
|
view.viewBackgroundColor = .white
|
|
view.wantsLayer = true
|
|
view.shadow = shadow
|
|
view.layer?.borderWidth = 2
|
|
view.layer?.borderColor = NSColor.white.cgColor
|
|
return view
|
|
}()
|
|
|
|
fileprivate var _radius: CGFloat?
|
|
fileprivate var backRadius: CGFloat {
|
|
if let radius = _radius { return radius }
|
|
return height / 2
|
|
}
|
|
|
|
fileprivate var circleRadius: CGFloat {
|
|
if let radius = _radius { return radius - outlineWidth }
|
|
return ( height - ( outlineWidth*2 ) ) / 2
|
|
}
|
|
|
|
fileprivate var toggleSize: CGFloat { height - ( outlineWidth * 2 ) }
|
|
|
|
// ================================================================================
|
|
// MARK: Callback
|
|
// ================================================================================
|
|
var callback: ((_ isOn: Bool) -> Void)?
|
|
|
|
// ================================================================================
|
|
// MARK: Public Parameters
|
|
// ================================================================================
|
|
@IBInspectable
|
|
public var isOn: Bool = false {
|
|
didSet { animate() }
|
|
}
|
|
|
|
/// Change the toggle border on and off
|
|
@IBInspectable
|
|
public var hasToggleBorder: Bool = true {
|
|
didSet { circle.layer?.borderWidth = hasToggleBorder ? toggleBorderWidth : 0 }
|
|
}
|
|
|
|
/// Change the width of the outline border
|
|
@IBInspectable public var outlineWidth: CGFloat = 2 {
|
|
didSet {
|
|
backVw.layer?.borderWidth = outlineWidth
|
|
layoutSwitch(resetingLayout: true)
|
|
}
|
|
}
|
|
|
|
/// Change the width of the border on the toggle
|
|
@IBInspectable public var toggleBorderWidth: CGFloat = 2 {
|
|
didSet { circle.layer?.borderWidth = hasToggleBorder ? toggleBorderWidth : 0 }
|
|
}
|
|
|
|
/// Change the radius of the complete toggle
|
|
@IBInspectable public var radius: CGFloat {
|
|
get {
|
|
if let radius = _radius { return radius }
|
|
return ( height - ( outlineWidth * 2 ) ) / 2
|
|
}
|
|
set {
|
|
_radius = newValue
|
|
layoutSwitch()
|
|
}
|
|
}
|
|
|
|
|
|
/// Change the color of the outline border
|
|
@IBInspectable public var outlineColor: NSColor = .lightGray {
|
|
didSet { backVw.layer?.borderColor = outlineColor.cgColor }
|
|
}
|
|
|
|
/// Change the color of the fill when the toggle is on
|
|
@IBInspectable public var fillColor: NSColor = .lightGray {
|
|
didSet { if isOn { backVw.layer?.borderColor = fillColor.cgColor } }
|
|
}
|
|
|
|
/// Change the color of the toggle center
|
|
@IBInspectable public var toggleColor: NSColor = .white {
|
|
didSet { circle.viewBackgroundColor = toggleColor }
|
|
}
|
|
|
|
/// Change the background color of the complete toggle (visible when switch is off)
|
|
@IBInspectable public var backColor: NSColor = .white {
|
|
didSet { backVw.viewBackgroundColor = backColor }
|
|
}
|
|
|
|
// ================================================================================
|
|
// MARK: Initialization
|
|
// ================================================================================
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
drawView()
|
|
}
|
|
|
|
required init(height: CGFloat = 44) {
|
|
self.height = height
|
|
super.init(frame: .zero)
|
|
drawView()
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
let push = Double(outlineWidth + width) - Double(height)
|
|
NSAnimationContext.runAnimationGroup({ (context) in
|
|
context.duration = 0.3
|
|
context.allowsImplicitAnimation = true
|
|
|
|
let adjustment = (toggleSize/4)
|
|
widthConstraint?.isActive = false
|
|
widthConstraint = circle.widthAnchor.constraint(equalToConstant: toggleSize+adjustment)
|
|
widthConstraint?.isActive = true
|
|
|
|
if isOn {
|
|
leftConstraint?.constant = CGFloat(push)-adjustment
|
|
}
|
|
animator().layoutSubtreeIfNeeded()
|
|
})
|
|
}
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
isOn = !isOn
|
|
}
|
|
|
|
// ================================================================================
|
|
// MARK: Helpers
|
|
// ================================================================================
|
|
fileprivate func drawView() {
|
|
backVw.viewBackgroundColor = backColor
|
|
|
|
addSubview(backVw)
|
|
backVw.translatesAutoresizingMaskIntoConstraints = false
|
|
backVw.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
|
|
backVw.widthAnchor.constraint(equalToConstant: width).isActive = true
|
|
backVw.heightAnchor.constraint(equalToConstant: height).isActive = true
|
|
|
|
addSubview(circle)
|
|
circle.translatesAutoresizingMaskIntoConstraints = false
|
|
leftConstraint = circle.leftAnchor.constraint(equalTo: backVw.leftAnchor, constant: outlineWidth)
|
|
circle.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
|
|
widthConstraint = circle.widthAnchor.constraint(equalToConstant: height-(outlineWidth*2))
|
|
heightConstraint = circle.heightAnchor.constraint(equalToConstant: height-(outlineWidth*2))
|
|
|
|
leftConstraint?.isActive = true
|
|
widthConstraint?.isActive = true
|
|
heightConstraint?.isActive = true
|
|
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
rightAnchor.constraint(equalTo: backVw.rightAnchor).isActive = true
|
|
heightAnchor.constraint(equalToConstant: height).isActive = true
|
|
|
|
layoutSwitch()
|
|
}
|
|
|
|
fileprivate func layoutSwitch(resetingLayout: Bool = false) {
|
|
if resetingLayout {
|
|
leftConstraint?.constant = outlineWidth
|
|
|
|
widthConstraint?.isActive = false
|
|
widthConstraint = circle.widthAnchor.constraint(equalToConstant: height-(outlineWidth*2))
|
|
widthConstraint?.isActive = true
|
|
|
|
heightConstraint?.isActive = false
|
|
heightConstraint = circle.heightAnchor.constraint(equalToConstant: height-(outlineWidth*2))
|
|
heightConstraint?.isActive = true
|
|
layoutSubtreeIfNeeded()
|
|
}
|
|
|
|
backVw.layer?.cornerRadius = backRadius
|
|
backVw.layer?.borderWidth = isOn ? (height/2) : outlineWidth
|
|
backVw.layer?.borderColor = outlineColor.cgColor
|
|
|
|
circle.layer?.cornerRadius = circleRadius
|
|
}
|
|
|
|
fileprivate func animate() {
|
|
acceptsTouchEvents = false
|
|
|
|
let push = Double(outlineWidth + width) - Double(height)
|
|
|
|
NSAnimationContext.runAnimationGroup({ (context) in
|
|
context.duration = 0.3
|
|
context.allowsImplicitAnimation = true
|
|
|
|
backVw.animator().layer?.borderWidth = isOn ? (height/2) : outlineWidth
|
|
backVw.animator().layer?.borderColor = isOn ? fillColor.cgColor : outlineColor.cgColor
|
|
|
|
widthConstraint?.isActive = false
|
|
widthConstraint = circle.widthAnchor.constraint(equalToConstant: toggleSize)
|
|
widthConstraint?.isActive = true
|
|
|
|
leftConstraint?.constant = isOn ? CGFloat(push) : outlineWidth
|
|
animator().layoutSubtreeIfNeeded()
|
|
}, completionHandler: {
|
|
self.acceptsTouchEvents = true
|
|
self.callback?(self.isOn)
|
|
})
|
|
}
|
|
|
|
}
|