Files
2022-04-25 07:28:13 +00:00

335 lines
13 KiB
Swift

//
// PreferencesViewController.swift
// PrivadoVPN
//
// Created by Murad Shabanov on 20.07.2021.
// Copyright © 2021 Privado LLC. All rights reserved.
//
import Foundation
import AppKit
protocol PreferencesViewInput: AnyObject {}
protocol PreferencesViewOutput: AnyObject {}
final class PreferencesViewController: NSViewController, PreferencesViewInput {
typealias HeaderCreationClosure = () -> NSViewController
enum Constants {
static let columnIdentifier = NSUserInterfaceItemIdentifier("PreferencesSegmentList")
static let cellIdentifier = NSUserInterfaceItemIdentifier("PreferencesSegmentCell")
}
var preferencePanes = [PreferencePane]() {
didSet {
self.segmentView.reloadData()
}
}
private var headerCreationClosure: HeaderCreationClosure
private var output: PreferencesViewOutput?
private var activeTabIndex: Int?
private let titleText: NSTextField
private let segmentView: NSTableView
private let controllerView: NSView
private var header: NSViewController?
private let line: View
// MARK: - JSNavigationBarViewControllerProvider
var navigationController: JSNavigationController?
init(output: PreferencesViewOutput, headerCreation: @escaping HeaderCreationClosure) {
self.titleText = NSTextField(frame: .zero)
self.segmentView = NSTableView()
self.controllerView = NSView(frame: .zero)
self.output = output
self.headerCreationClosure = headerCreation
self.line = View(frame: .zero)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
self.view = BottomView(backgroundColor: NSColor(red: 0, green: 8, blue: 44))
self.view.wantsLayer = true
}
override func viewDidLoad() {
self.configureUI()
self.bind()
self.layoutView()
}
override func viewDidAppear() {
if let superview = self.view.superview {
NSLayoutConstraint.activate([
self.view.topAnchor.constraint(equalTo: superview.topAnchor),
self.view.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
self.view.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
self.view.bottomAnchor.constraint(equalTo: superview.bottomAnchor)
])
}
}
private func configureUI() {
self.titleText.translatesAutoresizingMaskIntoConstraints = false
self.titleText.stringValue = NSLocalizedString("preference.identifier.preferences", comment: "")
self.titleText.backgroundColor = .clear
self.titleText.isBezeled = false
self.titleText.isEditable = false
self.titleText.sizeToFit()
self.titleText.textColor = .white
self.titleText.font = NSFont(name: PrivadoConstants.Font.light, size: 13.5)
self.segmentView.translatesAutoresizingMaskIntoConstraints = false
self.segmentView.backgroundColor = .clear
self.segmentView.selectionHighlightStyle = .none
self.segmentView.focusRingType = .none
self.segmentView.rowSizeStyle = .custom
if #available(OSX 11.0, *) {
self.segmentView.style = .fullWidth
}
self.segmentView.cornerView = NSView()
self.controllerView.translatesAutoresizingMaskIntoConstraints = false
self.controllerView.wantsLayer = true
self.line.translatesAutoresizingMaskIntoConstraints = false
self.line.viewBackgroundColor = NSColor(red: 21, green: 25, blue: 53)
}
private func bind() {
self.segmentView.delegate = self
self.segmentView.dataSource = self
let column = NSTableColumn(identifier: Constants.columnIdentifier)
column.width = 160
column.isEditable = false
self.segmentView.addTableColumn(column)
self.view.addSubview(self.segmentView)
self.view.addSubview(self.controllerView)
self.view.addSubview(self.line)
}
private func layoutView() {
NSLayoutConstraint.activate([
self.segmentView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.segmentView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.segmentView.widthAnchor.constraint(equalToConstant: 160),
//
self.line.widthAnchor.constraint(equalToConstant: 1),
self.line.heightAnchor.constraint(equalToConstant: 484),
self.line.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 20),
self.line.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20),
self.line.leadingAnchor.constraint(equalTo: self.segmentView.trailingAnchor),
//
self.controllerView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.controllerView.leadingAnchor.constraint(equalTo: self.line.trailingAnchor),
self.controllerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
self.controllerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func preferencePaneLabels() -> [String] {
var labels = [String]()
self.preferencePanes.forEach { labels.append($0.preferencePaneIdentifier.rawValue) }
return labels
}
func activateTab(preferenceIdentifier: PreferencePane.Identifier, animated: Bool) {
guard let index = (self.preferencePanes.firstIndex { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
return self.activateTab(index: 0, animated: animated)
}
self.animtionTabTranstition(index: index)
}
func activateTab(index: Int, animated: Bool) {
defer {
self.activeTabIndex = index
}
if self.activeTabIndex.isExist {
guard index != self.activeTabIndex else {
return
}
self.animtionTabTranstition(index: index)
} else {
self.immediatelyDisplayTab(index: index)
}
}
func restoreInitialTab() {
if !self.activeTabIndex.isExist {
self.activateTab(index: 0, animated: false)
}
}
private func animtionTabTranstition(index: Int) {
guard let activeTab = self.activeTabIndex else {
assertionFailure("animateTabTransition called before a tab was displayed; transition only works from one tab to another")
self.immediatelyDisplayTab(index: index)
return
}
let down = index > activeTab
let fromViewController = self.preferencePanes[activeTab]
let toViewController = self.preferencePanes[index]
let viewBounds = self.controllerView.bounds
let height = viewBounds.height
let duration = 0.3
fromViewController.view.wantsLayer = true
toViewController.view.wantsLayer = true
let fadeInAnimation = CABasicAnimation(keyPath: "opacity")
fadeInAnimation.fromValue = 0.0
fadeInAnimation.toValue = 1.0
fadeInAnimation.duration = duration
fadeInAnimation.timingFunction = .easeInEaseOut
fadeInAnimation.fillMode = .forwards
fadeInAnimation.isRemovedOnCompletion = false
let fadeOutAnimation = CABasicAnimation(keyPath: "opacity")
fadeOutAnimation.fromValue = 1.0
fadeOutAnimation.toValue = 0.0
fadeOutAnimation.duration = duration
fadeOutAnimation.timingFunction = .easeInEaseOut
fadeOutAnimation.fillMode = .forwards
fadeOutAnimation.isRemovedOnCompletion = false
let slideFromBottomAnimation = CABasicAnimation(keyPath: "position.y")
slideFromBottomAnimation.fromValue = -height
slideFromBottomAnimation.toValue = 0
slideFromBottomAnimation.duration = duration
slideFromBottomAnimation.timingFunction = .easeInEaseOut
slideFromBottomAnimation.fillMode = .forwards
slideFromBottomAnimation.isRemovedOnCompletion = false
let slideFromTopToBottom = CABasicAnimation(keyPath: "position.y")
slideFromTopToBottom.fromValue = height
slideFromTopToBottom.toValue = 0
slideFromTopToBottom.duration = duration
slideFromTopToBottom.timingFunction = .easeInEaseOut
slideFromTopToBottom.fillMode = .forwards
slideFromTopToBottom.isRemovedOnCompletion = false
let slideToBottomAnimation = CABasicAnimation(keyPath: "position.y")
slideToBottomAnimation.fromValue = 0
slideToBottomAnimation.toValue = -height
slideToBottomAnimation.duration = duration
slideToBottomAnimation.timingFunction = .easeInEaseOut
slideToBottomAnimation.fillMode = .forwards
slideToBottomAnimation.isRemovedOnCompletion = false
let slideToTopAnimation = CABasicAnimation(keyPath: "position.y")
slideToTopAnimation.fromValue = 0
slideToTopAnimation.toValue = height
slideToTopAnimation.duration = duration
slideToTopAnimation.timingFunction = .easeInEaseOut
slideToTopAnimation.fillMode = .forwards
slideToTopAnimation.isRemovedOnCompletion = false
self.controllerView.addSubview(toViewController.view, positioned: .above, relativeTo: fromViewController.view)
CATransaction.begin()
CATransaction.setCompletionBlock {
fromViewController.view.removeFromSuperview()
fromViewController.view.layer?.removeAllAnimations()
}
fromViewController.view.layer?.add(fadeOutAnimation, forKey: nil)
toViewController.view.layer?.add(fadeInAnimation, forKey: nil)
fromViewController.view.layer?.add(down ? slideToTopAnimation : slideToBottomAnimation, forKey: nil)
toViewController.view.layer?.add(down ? slideFromBottomAnimation : slideFromTopToBottom, forKey: nil)
CATransaction.commit()
}
private func immediatelyDisplayTab(index: Int) {
guard let viewController = self.preferencePanes[safe: index] else { return }
self.controllerView.addSubview(viewController.view)
}
private func animate(fromView: NSView?, toView: NSView?, animation: AnimationBlock) {
fromView?.wantsLayer = true
toView?.wantsLayer = true
for animation in animation(fromView, toView).fromViewAnimations {
fromView?.layer?.add(animation, forKey: nil)
}
for animation in animation(fromView, toView).toViewAnimations {
toView?.layer?.add(animation, forKey: nil)
}
}
@objc func tabSelected(_ sender: SegmentedControl) {
self.activateTab(index: sender.index, animated: true)
}
}
extension PreferencesViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard tableView.tableColumns[safe: 0] == tableColumn
, tableColumn.isExist else {
return nil
}
guard let title = self.preferencePanes[safe: row]?.preferencePaneIdentifier.rawValue else { return nil }
tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil)
let cell = SegmentCell(frame: .zero)
cell.update(with: title)
cell.cellIsSelected(self.activeTabIndex == row)
return cell
}
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
guard let selectedRow = self.activeTabIndex, row != selectedRow else { return false }
guard let previousView = self.segmentView.view(atColumn: 0, row: selectedRow, makeIfNecessary: true) as? SegmentCell,
let nextView = self.segmentView.view(atColumn: 0, row: row, makeIfNecessary: true) as? SegmentCell else { return false}
previousView.cellIsSelected(false)
nextView.cellIsSelected(true)
self.activateTab(index: row, animated: true )
return true
}
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
return 55
}
}
extension PreferencesViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return self.preferencePanes.count
}
}
extension PreferencesViewController: JSNavigationBarViewControllerProvider {
func navigationBarViewController() -> NSViewController {
if let header = self.header { return header }
let header = self.headerCreationClosure()
self.header = header
return header
}
}