332 lines
11 KiB
Swift
332 lines
11 KiB
Swift
//
|
|
// 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, 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
|
|
}
|
|
|
|
formRow.addArrangedSubview(label)
|
|
formRow.addArrangedSubview(view)
|
|
|
|
addArrangedSubview(formRow)
|
|
return formRow
|
|
}
|
|
}
|
|
|
|
public class BaseViewController<Component: UIView>: 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<AnyCancellable>()
|
|
|
|
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()
|
|
|
|
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()
|
|
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)
|
|
|
|
loadCustomRotors()
|
|
}
|
|
|
|
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, view: UIView) -> UIView {
|
|
return formStackView.addFormRow(label: label, 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 }
|
|
|
|
}
|