// // ModelViewController.swift // VDSSample // // Created by Matt Bruce on 8/15/22. // import Foundation import UIKit import Combine import VDS public class FormSection: UIStackView { public var title: String? { didSet { if let title { titleLabel.text = title titleLabel.isHidden = false } else { titleLabel.isHidden = true } } } private var titleLabel = Label().with { $0.isHidden = true; $0.textStyle = .boldBodyLarge; } public override init(frame: CGRect) { super.init(frame: frame) translatesAutoresizingMaskIntoConstraints = false alignment = .fill distribution = .fill axis = .vertical spacing = 10 addArrangedSubview(titleLabel) } public convenience init() { self.init(frame: .zero) } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @discardableResult open func addFormRow(label: String, tooltip: Tooltip.TooltipModel? = nil, view: UIView) -> UIView { let formRow = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.alignment = .fill $0.distribution = .fillEqually $0.axis = .horizontal $0.spacing = 5 } let label = Label().with { $0.tag = 1 $0.text = label $0.textStyle = .bodyLarge $0.numberOfLines = 0 if let tooltip { $0.addTooltip(tooltip) } } formRow.addArrangedSubview(label) formRow.addArrangedSubview(view) addArrangedSubview(formRow) return formRow } } public class BaseViewController: UIViewController, Initable , CustomRotorable { deinit { print("\(Self.self) deinit") } static func makeComponent() -> Component { Component() } private let edgeSpacing = 16.0 //-------------------------------------------------- // MARK: - Combine Properties //-------------------------------------------------- public var subscribers = Set() public var customRotors: [CustomRotorType] = [ CustomRotorType(name: "Links", trait: .link), CustomRotorType(name: "Buttons", trait: .button) ] //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- private var initialSetupPerformed = false //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(nibName: nil, bundle: nil) initialSetup() } public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) initialSetup() } public required init?(coder: NSCoder) { super.init(coder: coder) initialSetup() } //-------------------------------------------------- // MARK: - Setup //-------------------------------------------------- public func initialSetup() { overrideUserInterfaceStyle = .light if !initialSetupPerformed { initialSetupPerformed = true setup() contentTopView.backgroundColor = Surface.light.color } } public lazy var surfacePickerSelectorView = { SurfacePickerSelectorView(picker: self.picker) }() public var picker: UIPickerView = { return UIPickerView().with { $0.backgroundColor = .white $0.translatesAutoresizingMaskIntoConstraints = false } }() public var component = Component() lazy var debugViewSwitch = Toggle().with{ $0.onChange = { [weak self] sender in self?.showDebug(show: sender.isOn) } } open func showDebug(show: Bool) { self.component.debugBorder(show: show, color: .blue) } public var formStackView = FormSection() public lazy var stackView = UIStackView().with { $0.axis = .vertical $0.distribution = .fill $0.alignment = .fill $0.spacing = 0 $0.translatesAutoresizingMaskIntoConstraints = false } let bottomScrollView = UIScrollView().with { $0.translatesAutoresizingMaskIntoConstraints = false } public var contentTopView = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } public var contentBottomView = UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } open override func viewDidLoad() { super.viewDidLoad() if let component = component as? (any ViewProtocol), let content = VDSHelper.changeLog(for: type(of: component)) { let tooltip = VDS.Tooltip() tooltip.title = "ChangeLog" tooltip.content = content navigationItem.rightBarButtonItem = UIBarButtonItem(customView: tooltip) } view.backgroundColor = .white // Add the top and bottom views to the stack view stackView.addArrangedSubview(contentTopView.makeWrapper(edgeSpacing: 16, isTrailing: false)) stackView.addArrangedSubview(bottomScrollView) // Add the stack view to the view controller's view view.addSubview(stackView) // Pin the stack view to the edges of the view controller's view NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -25) ]) bottomScrollView.addSubview(contentBottomView) // Pin the content view to the edges of the scroll view NSLayoutConstraint.activate([ contentBottomView.leadingAnchor.constraint(equalTo: bottomScrollView.leadingAnchor), contentBottomView.trailingAnchor.constraint(equalTo: bottomScrollView.trailingAnchor), contentBottomView.topAnchor.constraint(equalTo: bottomScrollView.topAnchor), contentBottomView.bottomAnchor.constraint(equalTo: bottomScrollView.bottomAnchor), contentBottomView.widthAnchor.constraint(equalTo: bottomScrollView.widthAnchor) ]) contentBottomView.addSubview(formStackView) formStackView.pinToSuperView(.init(top: 0, left: 16, bottom: 0, right: edgeSpacing)) view.addSubview(picker) picker.pinBottom() picker.pinLeading() picker.pinTrailing() picker.isHidden = true setupForm() NotificationCenter.default .publisher(for: UIResponder.keyboardWillShowNotification) .sink { [weak self] notification in self?.keyboardWillShow(notification: notification) }.store(in: &subscribers) NotificationCenter.default .publisher(for: UIResponder.keyboardWillHideNotification) .sink { [weak self] notification in self?.keyboardWillHide(notification: notification) }.store(in: &subscribers) NotificationCenter.default .publisher(for: UITextField.textDidBeginEditingNotification) .sink { [weak self] notification in guard let self, let textField = notification.object as? UITextField else { return } self.activeTextField = textField }.store(in: &subscribers) NotificationCenter.default .publisher(for: UITextField.textDidEndEditingNotification) .sink { [weak self] notification in self?.activeTextField?.resignFirstResponder() self?.activeTextField = nil }.store(in: &subscribers) NotificationCenter.default.publisher(for: UIAccessibility.voiceOverStatusDidChangeNotification).sink { [weak self] _ in if UIAccessibility.isVoiceOverRunning { if let component = self?.component { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self?.loadCustomRotors() UIAccessibility.post(notification: .screenChanged, argument: component) } } } }.store(in: &subscribers) // Initially register the custom rotor if VoiceOver is on if UIAccessibility.isVoiceOverRunning { loadCustomRotors() UIAccessibility.post(notification: .screenChanged, argument: component) } if component.canBecomeFirstResponder { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tapGesture.cancelsTouchesInView = false // This allows the tap to pass through to other views. view.addGestureRecognizer(tapGesture) } } @objc func dismissKeyboard(_ sender: UITapGestureRecognizer) { let location = sender.location(in: self.view) // Check if the touch is outside the textView if !component.frame.contains(location) { component.resignFirstResponder() } } func isViewHiddenByKeyboard(view: UIView, keyboardFrame: CGRect) -> Bool { let viewFrameInWindow = view.convert(view.bounds, to: nil) let inetersectionFrame = viewFrameInWindow.intersection(keyboardFrame) return inetersectionFrame.height > 0 } func keyboardWillShow(notification: UIKit.Notification) { if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { if let activeTextField, self.view.frame.origin.y == 0, isViewHiddenByKeyboard(view: activeTextField, keyboardFrame: keyboardSize) { view.frame.origin.y -= keyboardSize.height } } } func keyboardWillHide(notification: UIKit.Notification) { if self.view.frame.origin.y != 0 { view.frame.origin.y = 0 } } public func setupForm() { addFormRow(label: "Show Bounds", view: debugViewSwitch) } let actionLabel = Label() @discardableResult public func addActionRow() -> UIView { addFormRow(label: "Action", view: actionLabel) } public func scrollToBottom() { let bottomOffset = CGPoint(x: 0, y: bottomScrollView.contentSize.height - bottomScrollView.bounds.height + bottomScrollView.contentInset.bottom) bottomScrollView.setContentOffset(bottomOffset, animated: true) } private func embed(_ viewController: UIViewController) { addChild(viewController) view.addSubview(viewController.view) viewController.view.translatesAutoresizingMaskIntoConstraints = false viewController.view.pinToSuperView() viewController.didMove(toParent: self) } open func addContentTopView(view: UIView, edgeSpacing: CGFloat = 16.0) { view.translatesAutoresizingMaskIntoConstraints = false contentTopView.addSubview(view) view.pinToSuperView(.uniform(edgeSpacing)) } open func append(section: FormSection) { formStackView.addArrangedSubview(section) } @discardableResult open func addFormRow(label: String, tooltip: Tooltip.TooltipModel? = nil, view: UIView) -> UIView { return formStackView.addFormRow(label: label,tooltip: tooltip, view: view) } var activeTextField: UITextField? /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open func setup() { if let textFields = allTextFields()?.filter({ $0.isKind(of: TextField.self) == false || $0.isKind(of: NumericField.self) }) { for textField in textFields { let keypadToolbar: UIToolbar = UIToolbar() // add a done button to the numberpad keypadToolbar.items=[ UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: self, action: nil), UIBarButtonItem(title: "Done", style: UIBarButtonItem.Style.done, target: textField, action: #selector(UITextField.resignFirstResponder)) ] keypadToolbar.sizeToFit() // add a toolbar with a done button above the number pad textField.inputAccessoryView = keypadToolbar textField.keyboardType = textField.isNumeric ? .numberPad : .alphabet textField.returnKeyType = .done } } } open func updateView() { //print("\(Self.self) updateView()") } open func allTextFields() -> [TextField]? { nil } }