Files
2021-06-02 12:28:09 +03:00

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)
})
}
}