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