refactored to have a scrolling viewcontroller

updated checkboxgroup

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2022-08-24 11:08:05 -05:00
parent cd62738818
commit 5ed2384db3
13 changed files with 738 additions and 82 deletions

View File

@ -40,8 +40,16 @@
EA3C3BB528996775000CA526 /* StoryboardInitable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3C3BB128996775000CA526 /* StoryboardInitable.swift */; };
EA3C3BB628996775000CA526 /* MenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3C3BB228996775000CA526 /* MenuViewController.swift */; };
EA3C3BB728996775000CA526 /* ToggleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA3C3BB328996775000CA526 /* ToggleViewController.swift */; };
EA89200A28B52934006B9984 /* CheckboxGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89200928B52934006B9984 /* CheckboxGroupViewController.swift */; };
EA89201928B56DF5006B9984 /* RadioBoxGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89201828B56DF5006B9984 /* RadioBoxGroupViewController.swift */; };
EA89204628B66CE2006B9984 /* ScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89203F28B66CE2006B9984 /* ScrollViewController.swift */; };
EA89204728B66CE2006B9984 /* KeyboardFrameChangeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204028B66CE2006B9984 /* KeyboardFrameChangeListener.swift */; };
EA89204828B66CE2006B9984 /* ScrollViewKeyboardAvoiding.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204128B66CE2006B9984 /* ScrollViewKeyboardAvoiding.swift */; };
EA89204928B66CE2006B9984 /* KeyboardFrameChangeListening.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204228B66CE2006B9984 /* KeyboardFrameChangeListening.swift */; };
EA89204A28B66CE2006B9984 /* KeyboardFrameChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204328B66CE2006B9984 /* KeyboardFrameChange.swift */; };
EA89204B28B66CE2006B9984 /* ScrollViewKeyboardAvoider.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204428B66CE2006B9984 /* ScrollViewKeyboardAvoider.swift */; };
EA89204C28B66CE2006B9984 /* ScrollWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204528B66CE2006B9984 /* ScrollWrapperView.swift */; };
EA89204E28B67332006B9984 /* CheckBoxGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89204D28B67332006B9984 /* CheckBoxGroupViewController.swift */; };
EA89205128B68307006B9984 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA89205028B68307006B9984 /* TextField.swift */; };
EAB1D2C528A6B11D00DAE764 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2C428A6B11D00DAE764 /* TestViewController.swift */; };
EAB1D2C928AAAA1D00DAE764 /* ModelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2C828AAAA1D00DAE764 /* ModelViewController.swift */; };
EAB1D2CB28AAB9E200DAE764 /* TemplateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CA28AAB9E200DAE764 /* TemplateViewController.swift */; };
@ -95,8 +103,16 @@
EA3C3BBA289968A0000CA526 /* VDSTypographyTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSTypographyTokens.xcframework; path = ../SharedFrameworks/VDSTypographyTokens.xcframework; sourceTree = "<group>"; };
EA3C3BBB289968A0000CA526 /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = "<group>"; };
EA3C3BC3289968B1000CA526 /* VDS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = VDS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EA89200928B52934006B9984 /* CheckboxGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxGroupViewController.swift; sourceTree = "<group>"; };
EA89201828B56DF5006B9984 /* RadioBoxGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioBoxGroupViewController.swift; sourceTree = "<group>"; };
EA89203F28B66CE2006B9984 /* ScrollViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewController.swift; sourceTree = "<group>"; };
EA89204028B66CE2006B9984 /* KeyboardFrameChangeListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardFrameChangeListener.swift; sourceTree = "<group>"; };
EA89204128B66CE2006B9984 /* ScrollViewKeyboardAvoiding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewKeyboardAvoiding.swift; sourceTree = "<group>"; };
EA89204228B66CE2006B9984 /* KeyboardFrameChangeListening.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardFrameChangeListening.swift; sourceTree = "<group>"; };
EA89204328B66CE2006B9984 /* KeyboardFrameChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardFrameChange.swift; sourceTree = "<group>"; };
EA89204428B66CE2006B9984 /* ScrollViewKeyboardAvoider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollViewKeyboardAvoider.swift; sourceTree = "<group>"; };
EA89204528B66CE2006B9984 /* ScrollWrapperView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollWrapperView.swift; sourceTree = "<group>"; };
EA89204D28B67332006B9984 /* CheckBoxGroupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxGroupViewController.swift; sourceTree = "<group>"; };
EA89205028B68307006B9984 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
EAB1D2C428A6B11D00DAE764 /* TestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = "<group>"; };
EAB1D2C828AAAA1D00DAE764 /* ModelViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewController.swift; sourceTree = "<group>"; };
EAB1D2CA28AAB9E200DAE764 /* TemplateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateViewController.swift; sourceTree = "<group>"; };
@ -156,6 +172,7 @@
children = (
EAF7F0C3289DA24F00B287F5 /* Supporting Files */,
EAF7F07E28996A0700B287F5 /* Protocols */,
EA89204F28B682F4006B9984 /* Classes */,
EAF7F07F28996A1900B287F5 /* ViewControllers */,
EA3C3B9C289966EF000CA526 /* AppDelegate.swift */,
EA3C3B9E289966EF000CA526 /* SceneDelegate.swift */,
@ -184,6 +201,28 @@
name = Frameworks;
sourceTree = "<group>";
};
EA89203E28B66CE2006B9984 /* ScrollViewController */ = {
isa = PBXGroup;
children = (
EA89203F28B66CE2006B9984 /* ScrollViewController.swift */,
EA89204028B66CE2006B9984 /* KeyboardFrameChangeListener.swift */,
EA89204128B66CE2006B9984 /* ScrollViewKeyboardAvoiding.swift */,
EA89204228B66CE2006B9984 /* KeyboardFrameChangeListening.swift */,
EA89204328B66CE2006B9984 /* KeyboardFrameChange.swift */,
EA89204428B66CE2006B9984 /* ScrollViewKeyboardAvoider.swift */,
EA89204528B66CE2006B9984 /* ScrollWrapperView.swift */,
);
path = ScrollViewController;
sourceTree = "<group>";
};
EA89204F28B682F4006B9984 /* Classes */ = {
isa = PBXGroup;
children = (
EA89205028B68307006B9984 /* TextField.swift */,
);
path = Classes;
sourceTree = "<group>";
};
EAF7F0792899698800B287F5 /* Resources */ = {
isa = PBXGroup;
children = (
@ -207,8 +246,9 @@
EAF7F07F28996A1900B287F5 /* ViewControllers */ = {
isa = PBXGroup;
children = (
EA89203E28B66CE2006B9984 /* ScrollViewController */,
EA3C3BB228996775000CA526 /* MenuViewController.swift */,
EA89200928B52934006B9984 /* CheckboxGroupViewController.swift */,
EA89204D28B67332006B9984 /* CheckBoxGroupViewController.swift */,
EAF7F09B2899B92400B287F5 /* CheckboxViewController.swift */,
EAB1D2D328AC409F00DAE764 /* LabelViewController.swift */,
EAB1D2C828AAAA1D00DAE764 /* ModelViewController.swift */,
@ -362,19 +402,27 @@
buildActionMask = 2147483647;
files = (
EA3C3BB728996775000CA526 /* ToggleViewController.swift in Sources */,
EA89204C28B66CE2006B9984 /* ScrollWrapperView.swift in Sources */,
EA89205128B68307006B9984 /* TextField.swift in Sources */,
EA3C3BB528996775000CA526 /* StoryboardInitable.swift in Sources */,
EA89201928B56DF5006B9984 /* RadioBoxGroupViewController.swift in Sources */,
EA3C3BB628996775000CA526 /* MenuViewController.swift in Sources */,
EA3C3B9D289966EF000CA526 /* AppDelegate.swift in Sources */,
EAF7F11A28A14A0E00B287F5 /* RadioButtonViewController.swift in Sources */,
EAB1D2CB28AAB9E200DAE764 /* TemplateViewController.swift in Sources */,
EA89204628B66CE2006B9984 /* ScrollViewController.swift in Sources */,
EA3C3B9F289966EF000CA526 /* SceneDelegate.swift in Sources */,
EA89204A28B66CE2006B9984 /* KeyboardFrameChange.swift in Sources */,
EA3C3BB428996775000CA526 /* PickerBase.swift in Sources */,
EAB1D2C528A6B11D00DAE764 /* TestViewController.swift in Sources */,
EAB1D2C928AAAA1D00DAE764 /* ModelViewController.swift in Sources */,
EA89200A28B52934006B9984 /* CheckboxGroupViewController.swift in Sources */,
EA89204728B66CE2006B9984 /* KeyboardFrameChangeListener.swift in Sources */,
EA89204828B66CE2006B9984 /* ScrollViewKeyboardAvoiding.swift in Sources */,
EAF7F09C2899B92400B287F5 /* CheckboxViewController.swift in Sources */,
EA89204E28B67332006B9984 /* CheckBoxGroupViewController.swift in Sources */,
EA89204928B66CE2006B9984 /* KeyboardFrameChangeListening.swift in Sources */,
EAB1D2D428AC409F00DAE764 /* LabelViewController.swift in Sources */,
EA89204B28B66CE2006B9984 /* ScrollViewKeyboardAvoider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,32 @@
//
// TextField.swift
// VDSSample
//
// Created by Matt Bruce on 8/24/22.
//
import Foundation
import UIKit
class TextField: UITextField {
var textPadding = UIEdgeInsets(
top: 10,
left: 10,
bottom: 10,
right: 10
)
override func textRect(forBounds bounds: CGRect) -> CGRect {
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
let rect = super.textRect(forBounds: bounds)
return rect.inset(by: textPadding)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
let rect = super.editingRect(forBounds: bounds)
return rect.inset(by: textPadding)
}
}

View File

@ -44,6 +44,28 @@ class PickerBase<EnumType: RawRepresentable>: NSObject, PickerViewable, UIPicker
}
}
class PickerSelectorView: UIStackView {
var label = UILabel()
var button = UIButton(type: .system).with { instance in
instance.configuration = .filled()
instance.setTitle("Select", for: .normal)
}
init(title: String){
super.init(frame: .zero)
self.axis = .horizontal
self.distribution = .fillEqually
self.alignment = .fill
label.text = title
addArrangedSubview(label)
addArrangedSubview(button)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class SurfacePicker: PickerBase<Surface> {
init(){
super.init(items: [.light, .dark])

View File

@ -1,8 +1,8 @@
//
// CheckboxViewController.swift
// CheckBoxGroup2ViewController.swift
// VDSSample
//
// Created by Matt Bruce on 8/1/22.
// Created by Matt Bruce on 8/24/22.
//
import Foundation
@ -11,33 +11,69 @@ import VDS
import VDSColorTokens
import Combine
class CheckboxGroupViewController: ModelViewController<DefaultCheckboxGroupModel>, StoryboardInitable {
class CheckboxGroupViewController: ModelScrollViewController<DefaultCheckboxGroupModel> {
enum PickerType {
case surface
}
static var storyboardId: String = "checkboxGroup"
static var storyboardName: String = "Components"
@IBOutlet weak var checkboxContainerView: UIView!
@IBOutlet weak var picker: UIPickerView!
@IBOutlet weak var surfaceLabel: UILabel!
@IBOutlet weak var disabledSwitch: UISwitch!
@IBOutlet weak var labelTextField: UITextField!
@IBOutlet weak var childTextField: UITextField!
@IBOutlet weak var showErrorSwitch: UISwitch!
var picker = UIPickerView()
var surfacePickerSelectorView = PickerSelectorView(title: "light")
var disabledSwitch = UISwitch()
var labelTextField = TextField()
var childTextField = TextField()
var showErrorSwitch = UISwitch()
var checkboxGroup = CheckboxGroup()
override func viewDidLoad() {
super.viewDidLoad()
checkboxContainerView.addSubview(checkboxGroup)
checkboxGroup.leadingAnchor.constraint(equalTo: checkboxContainerView.leadingAnchor, constant: 10).isActive = true
checkboxGroup.bottomAnchor.constraint(equalTo: checkboxContainerView.bottomAnchor, constant: -20).isActive = true
checkboxGroup.topAnchor.constraint(equalTo: checkboxContainerView.topAnchor, constant: 20).isActive = true
checkboxGroup.trailingAnchor.constraint(equalTo: checkboxContainerView.trailingAnchor, constant: 10).isActive = true
view.addGestureRecognizer(UITapGestureRecognizer(target: self.view, action: #selector(UIView.endEditing(_:))))
addContentTopView(view: checkboxGroup)
addFormRow(label: "Disabled", view: disabledSwitch)
addFormRow(label: "Surface", view: surfacePickerSelectorView)
addFormRow(label: "Label Text", view: labelTextField)
addFormRow(label: "Childe Text", view: childTextField)
addFormRow(label: "Error", view: showErrorSwitch)
checkboxGroup
.handlerPublisher()
.sink { [weak self] viewModel in
self?.model = viewModel
}.store(in: &subscribers)
showErrorSwitch
.publisher(for: .valueChanged)
.sink { [weak self] sender in
self?.checkboxGroup.hasError = sender.isOn
}.store(in: &subscribers)
disabledSwitch
.publisher(for: .valueChanged)
.sink { [weak self] sender in
self?.checkboxGroup.disabled = sender.isOn
}.store(in: &subscribers)
labelTextField
.textPublisher
.sink { [weak self] text in
self?.checkbox?.labelText = text
}.store(in: &subscribers)
childTextField
.textPublisher
.sink { [weak self] text in
self?.checkbox?.childText = text
}.store(in: &subscribers)
surfacePickerSelectorView.button
.publisher(for: .touchUpInside)
.sink { [weak self] _ in
self?.pickerType = .surface
}.store(in: &subscribers)
setupPicker()
setupModel()
}
@ -55,15 +91,9 @@ class CheckboxGroupViewController: ModelViewController<DefaultCheckboxGroupMode
model2.childText = "Apple iPhone 11 - 128 GB\nOtterbox Case Black\nScreen Protector"
defaultModel.selectors = [model1, model2]
set(with: defaultModel)
checkboxGroup
.handlerPublisher()
.sink { [weak self] viewModel in
self?.model = viewModel
}.store(in: &subscribers)
//setup UI
surfaceLabel.text = model.surface.rawValue
surfacePickerSelectorView.label.text = model.surface.rawValue
disabledSwitch.isOn = model.disabled
labelTextField.text = model2.labelText
childTextField.text = model1.childText
@ -80,31 +110,8 @@ class CheckboxGroupViewController: ModelViewController<DefaultCheckboxGroupMode
checkboxGroup.selectorViews.first
}
@IBAction func disabledChanged(_ sender: UISwitch) {
checkboxGroup.disabled = sender.isOn
}
@IBAction func onLabelTextDidEnd(_ sender: UITextField) {
checkbox?.labelText = sender.text
sender.resignFirstResponder()
}
@IBAction func onChildTextDidEnd(_ sender: UITextField) {
checkbox?.childText = sender.text
sender.resignFirstResponder()
}
@IBAction func showErrorChanged(_ sender: UISwitch) {
checkboxGroup.hasError = sender.isOn
}
@IBAction func surfaceClick(_ sender: Any) {
pickerType = .surface
}
//Picker
var surfacePicker = SurfacePicker()
var surfacePicker = SurfacePicker()
var pickerType: PickerType = .surface {
didSet {
func update(object: UIPickerViewDelegate & UIPickerViewDataSource){
@ -123,12 +130,13 @@ class CheckboxGroupViewController: ModelViewController<DefaultCheckboxGroupMode
}
func setupPicker(){
contentStackView.addArrangedSubview(picker)
picker.isHidden = true
surfacePicker.onPickerDidSelect = { [weak self] item in
self?.checkboxGroup.surface = item
self?.checkboxContainerView.backgroundColor = item.color
self?.surfaceLabel.text = item.rawValue
self?.contentTopView.backgroundColor = item.color
self?.surfacePickerSelectorView.label.text = item.rawValue
}
}
}
}

View File

@ -77,3 +77,161 @@ public class ModelViewController<ModelType: Modelable>: UIViewController, ModelH
open func updateView(viewModel: ModelType) {}
}
public class ModelScrollViewController<ModelType: Modelable>: UIViewController, ModelHandlerable, Initable {
deinit {
print("\(Self.self) deinit")
}
//--------------------------------------------------
// MARK: - Combine Properties
//--------------------------------------------------
@Published public var model: ModelType = ModelType()
public var modelPublisher: Published<ModelType>.Publisher { $model }
public var subscribers = Set<AnyCancellable>()
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
private var initialSetupPerformed = false
@Proxy(\.model.surface)
open var surface: Surface
@Proxy(\.model.disabled)
open var disabled: Bool
//--------------------------------------------------
// MARK: - Initializers
//--------------------------------------------------
required public init() {
super.init(nibName: nil, bundle: nil)
initialSetup()
set(with: model)
}
public required init(with model: ModelType) {
super.init(nibName: nil, bundle: nil)
initialSetup()
set(with: model)
}
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() {
if !initialSetupPerformed {
initialSetupPerformed = true
setupUpdateView()
setup()
}
}
public var contentStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .fill
$0.distribution = .fillProportionally
$0.axis = .vertical
}
}()
public var formStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .fill
$0.distribution = .fillProportionally
$0.axis = .vertical
$0.spacing = 10
}
}()
public var contentTopView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
public var contentBottomView: UIView = {
return UIView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
}
}()
open override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
embed(scrollViewController)
scrollViewController.scrollView.alwaysBounceVertical = true
scrollViewController.contentView = contentStackView
contentStackView.addArrangedSubview(contentTopView)
contentStackView.addArrangedSubview(contentBottomView)
contentBottomView.addSubview(formStackView)
formStackView.translatesAutoresizingMaskIntoConstraints = false
formStackView.topAnchor.constraint(equalTo: contentBottomView.topAnchor, constant: 16).isActive = true
formStackView.leadingAnchor.constraint(equalTo: contentBottomView.leadingAnchor, constant: 16).isActive = true
formStackView.trailingAnchor.constraint(equalTo: contentBottomView.trailingAnchor, constant: -16).isActive = true
formStackView.bottomAnchor.constraint(equalTo: contentBottomView.bottomAnchor, constant: -16).isActive = true
}
private let scrollViewController = ScrollViewController()
private func embed(_ viewController: UIViewController) {
addChild(viewController)
view.addSubview(viewController.view)
viewController.view.translatesAutoresizingMaskIntoConstraints = false
viewController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
viewController.didMove(toParent: self)
}
open func addContentTopView(view: UIView) {
contentTopView.addSubview(view)
view.leadingAnchor.constraint(equalTo: contentTopView.leadingAnchor, constant: 16).isActive = true
view.trailingAnchor.constraint(equalTo: contentTopView.trailingAnchor, constant: -16).isActive = true
view.topAnchor.constraint(equalTo: contentTopView.topAnchor, constant: 20).isActive = true
view.bottomAnchor.constraint(equalTo: contentTopView.bottomAnchor, constant: -20).isActive = true
}
open func addFormRow(label: String, view: UIView) {
let formRow = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .fill
$0.distribution = .fillEqually
$0.axis = .horizontal
$0.spacing = 5
}
let label = UILabel().with {
$0.text = label
}
formRow.addArrangedSubview(label)
formRow.addArrangedSubview(view)
formStackView.addArrangedSubview(formRow)
}
open func setup() {}
open func shouldUpdateView(viewModel: ModelType) -> Bool { true }
open func updateView(viewModel: ModelType) {}
}

View File

@ -0,0 +1,21 @@
import CoreGraphics
import Foundation
/// Represents keyboard frame change.
public struct KeyboardFrameChange {
/// Create new frame-change object.
///
/// - Parameters:
/// - frame: new keyboard frame
/// - animationDuration: change frame animation duration
public init(frame: CGRect, animationDuration: TimeInterval) {
self.frame = frame
self.animationDuration = animationDuration
}
/// New keyboard frame.
public let frame: CGRect
/// Frame change animation duration.
public let animationDuration: TimeInterval
}

View File

@ -0,0 +1,47 @@
import UIKit
/// `KeyboardFrameChangeListining` implementation.
public final class KeyboardFrameChangeListener: KeyboardFrameChangeListening {
/// Create new listener.
///
/// - Parameter notificationCenter: Source of keyboard frame change notifications.
public init(notificationCenter: NotificationCenter) {
self.notificationCenter = notificationCenter
observe()
}
// MARK: - KeyboardFrameChangeListening
public var keyboardFrameWillChange: ((KeyboardFrameChange) -> Void)?
// MARK: - Internals
private let notificationCenter: NotificationCenter
private var token: NSObjectProtocol?
private func observe() {
token = notificationCenter.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
object: nil,
queue: nil,
using: { [weak self] in self?.handle($0) }
)
}
private func handle(_ notification: Notification) {
guard let endFrame = notification.keyboardFrameEnd,
let animationDuration = notification.keyboardAnimationDuration else { return }
let change = KeyboardFrameChange(frame: endFrame, animationDuration: animationDuration)
keyboardFrameWillChange?(change)
}
}
private extension Notification {
var keyboardFrameEnd: CGRect? {
return userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
}
var keyboardAnimationDuration: Double? {
return userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
}
}

View File

@ -0,0 +1,5 @@
/// Listens for keyboard frame changes.
public protocol KeyboardFrameChangeListening: AnyObject {
/// Called when keyboard frame is about to change.
var keyboardFrameWillChange: ((KeyboardFrameChange) -> Void)? { get set }
}

View File

@ -0,0 +1,93 @@
import UIKit
/// Scroll View Controller.
public class ScrollViewController: UIViewController, UIScrollViewDelegate {
/// Animates using provided duration and closure.
public typealias Animator = (TimeInterval, @escaping () -> Void) -> Void
/// Create new instance.
///
/// - Parameters:
/// - keyboardFrameChangeListener: Used to observe keybaord frame changes.
/// - scrollViewKeyboardAvoider: Used to apply keyboard-avoiding insets to `UIScrollView`.
/// - wrapperViewFactory: Used to create `ScrollWrapperView`.
/// - animator: Closure used to animate layout changes.
public init(keyboardFrameChangeListener: KeyboardFrameChangeListening = KeyboardFrameChangeListener(notificationCenter: NotificationCenter.default),
scrollViewKeyboardAvoider: ScrollViewKeyboardAvoiding = ScrollViewKeyboardAvoider(animator: { UIView.animate(withDuration: $0, animations: $1) }),
wrapperViewFactory: @escaping () -> ScrollWrapperView = { ScrollWrapperView() },
animator: @escaping Animator = { UIView.animate(withDuration: $0, animations: $1) }) {
self.keyboardFrameChangeListener = keyboardFrameChangeListener
self.scrollViewKeyboardAvoider = scrollViewKeyboardAvoider
self.createWrapperView = wrapperViewFactory
self.animate = animator
super.init(nibName: nil, bundle: nil)
}
/// Does nothing, this class is designed to be used programmatically.
required public init?(coder aDecoder: NSCoder) { nil }
// MARK: - View
override public func loadView() {
view = createWrapperView()
}
override public func viewDidLoad() {
super.viewDidLoad()
wrapperView.scrollView.delegate = self
keyboardFrameChangeListener.keyboardFrameWillChange = { [unowned self] change in
self.scrollViewKeyboardAvoider.handleKeyboardFrameChange(
change.frame,
animationDuration: change.animationDuration,
for: self.wrapperView.scrollView
)
self.updateVisibleContentInset(scrollView: self.wrapperView.scrollView)
self.animate(change.animationDuration) {
self.wrapperView.layoutIfNeeded()
}
}
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateVisibleContentInset(scrollView: wrapperView.scrollView)
}
/// Contained `UIScrollView`.
public var scrollView: UIScrollView {
return wrapperView.scrollView
}
/// Scrollable content view.
public var contentView: UIView? {
get { return wrapperView.contentView }
set { wrapperView.contentView = newValue }
}
/// Main view of this view controller (non-scrollable).
public var wrapperView: ScrollWrapperView! {
return view as? ScrollWrapperView
}
// MARK: - UIScrollViewDelegate
@available(iOS 11.0, *)
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
updateVisibleContentInset(scrollView: scrollView)
}
// MARK: - Internals
private let keyboardFrameChangeListener: KeyboardFrameChangeListening
private let scrollViewKeyboardAvoider: ScrollViewKeyboardAvoiding
private let createWrapperView: () -> ScrollWrapperView
private let animate: Animator
private func updateVisibleContentInset(scrollView: UIScrollView) {
if #available(iOS 11.0, *) {
wrapperView.visibleContentInsets = scrollView.adjustedContentInset
} else {
wrapperView.visibleContentInsets = scrollView.contentInset
}
}
}

View File

@ -0,0 +1,42 @@
import UIKit
/// `ScrollViewKeyboardAvoiding` implementation.
public final class ScrollViewKeyboardAvoider: ScrollViewKeyboardAvoiding {
/// Animates using provided duration and closure
public typealias Animator = (TimeInterval, @escaping () -> Void) -> Void
/// Create new avoider
///
/// - Parameter animator: used to perform animations
public init(animator: @escaping Animator) {
self.animate = animator
}
// MARK: - ScrollViewKeyboardAvoiding
public func handleKeyboardFrameChange(
_ frame: CGRect,
animationDuration: TimeInterval,
for scrollView: UIScrollView
) {
guard let superview = scrollView.superview else { return }
let keyboardFrame = superview.convert(frame, from: nil)
var insets = scrollView.contentInset
let bottomCoverage = scrollView.frame.maxY - keyboardFrame.minY
let safeAreaInsets: UIEdgeInsets
if #available(iOS 11.0, *) {
safeAreaInsets = scrollView.safeAreaInsets
} else {
safeAreaInsets = .zero
}
insets.bottom = max(0, bottomCoverage - safeAreaInsets.bottom)
animate(animationDuration) {
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
}
// MARK: - Internals
private let animate: Animator
}

View File

@ -0,0 +1,12 @@
import UIKit
/// Adjusts insets of `UIScrollView` so the keyboard does not cover content.
public protocol ScrollViewKeyboardAvoiding {
/// Handle keyboard frame change.
///
/// - Parameters:
/// - frame: New frame of the keyboard.
/// - animationDuration: Frame change animation duration.
/// - scrollView: Target `UIScrollView`.
func handleKeyboardFrameChange(_ frame: CGRect, animationDuration: TimeInterval, for scrollView: UIScrollView)
}

View File

@ -0,0 +1,191 @@
import UIKit
/// `UIScrollView` wrapper that allows configuring how the scrollable content is laid out.
public class ScrollWrapperView: UIView {
/// Create `UIScrollView` wrapper view.
public init() {
scrollView = UIScrollView(frame: .zero)
super.init(frame: .zero)
scrollView.keyboardDismissMode = .interactive
addSubview(scrollView)
scrollView.addSubview(contentWrapperView)
setupLayout()
}
/// Does nothing, this class is designed to be used programmatically.
required public init?(coder aDecoder: NSCoder) { nil }
// MARK: - Subviews
/// Wrapped `UIScrollView`.
public let scrollView: UIScrollView
/// Scrollable content view.
public var contentView: UIView? {
didSet {
oldValue?.removeFromSuperview()
contentViewTopEqualSuper = nil
contentViewTopGreaterThanSuper = nil
contentViewLeft = nil
contentViewRight = nil
contentViewBottom = nil
if let newValue = contentView {
contentWrapperView.addSubview(newValue)
setupLayout(contentView: newValue)
}
}
}
let contentWrapperView = UIView()
// MARK: - Layout configuration
/// If `true`, `contentView` will be stretched to fill visible area.
///
/// Default is `true`.
public var contentViewStretching = true {
didSet { contentWrapperHeight.isActive = contentViewStretching }
}
/// If `true` the content view will be aligned to the bottom of scrollable area.
///
/// Default is `false`.
///
/// If the `contentViewStretching` is set to `false` this property makes no changes to the alignemnt.
public var alignContentToBottom = false {
didSet {
contentViewTopGreaterThanSuper?.isActive = alignContentToBottom == true
contentViewTopEqualSuper?.isActive = alignContentToBottom == false
}
}
/// Scrollable content insets.
///
/// Default is `.zero` which means no insets.
public var contentInsets: UIEdgeInsets = .zero {
didSet {
contentViewTopEqualSuper?.constant = contentInsets.top
contentViewTopGreaterThanSuper?.constant = contentInsets.top
contentViewLeft?.constant = contentInsets.left
contentViewRight?.constant = -contentInsets.right
contentViewBottom?.constant = -contentInsets.bottom
}
}
// MARK: - Touch handling configuration
/// If `true` touches outside the `contentView` will be handled and allow scrolling.
///
/// Default is `true`.
public var handlesTouchesOutsideContent = true
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if handlesTouchesOutsideContent {
return super.hitTest(point, with: event)
}
if let contentView = contentView, contentView.bounds.contains(convert(point, to: contentView)) {
return super.hitTest(point, with: event)
}
return nil
}
// MARK: - Internals
var visibleContentInsets: UIEdgeInsets {
get {
UIEdgeInsets(
top: visibleContentLayoutGuideTop.constant,
left: visibleContentLayoutGuideLeft.constant,
bottom: -visibleContentLayoutGuideBottom.constant,
right: -visibleContentLayoutGuideRight.constant
)
}
set {
visibleContentLayoutGuideTop.constant = newValue.top
visibleContentLayoutGuideLeft.constant = newValue.left
visibleContentLayoutGuideRight.constant = -newValue.right
visibleContentLayoutGuideBottom.constant = -newValue.bottom
}
}
private let visibleContentLayoutGuide = UILayoutGuide()
private var visibleContentLayoutGuideTop: NSLayoutConstraint!
private var visibleContentLayoutGuideLeft: NSLayoutConstraint!
private var visibleContentLayoutGuideRight: NSLayoutConstraint!
private var visibleContentLayoutGuideBottom: NSLayoutConstraint!
private var contentWrapperHeight: NSLayoutConstraint!
private var contentViewTopEqualSuper: NSLayoutConstraint?
private var contentViewTopGreaterThanSuper: NSLayoutConstraint?
private var contentViewLeft: NSLayoutConstraint?
private var contentViewRight: NSLayoutConstraint?
private var contentViewBottom: NSLayoutConstraint?
private func setupLayout() {
setupVisibleContentLayoutGuide()
setupScrollViewLayout()
setupContentWrapperViewLayout()
}
private func setupScrollViewLayout() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: topAnchor).isActive = true
scrollView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
}
private func setupContentWrapperViewLayout() {
contentWrapperView.translatesAutoresizingMaskIntoConstraints = false
contentWrapperView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentWrapperView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
contentWrapperView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
contentWrapperView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentWrapperView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
contentWrapperHeight = contentWrapperView.heightAnchor.constraint(
greaterThanOrEqualTo: visibleContentLayoutGuide.heightAnchor
)
contentWrapperHeight.isActive = contentViewStretching
}
private func setupVisibleContentLayoutGuide() {
addLayoutGuide(visibleContentLayoutGuide)
visibleContentLayoutGuideTop = visibleContentLayoutGuide.topAnchor.constraint(equalTo: topAnchor)
visibleContentLayoutGuideTop.isActive = true
visibleContentLayoutGuideLeft = visibleContentLayoutGuide.leftAnchor.constraint(equalTo: leftAnchor)
visibleContentLayoutGuideLeft.isActive = true
visibleContentLayoutGuideRight = visibleContentLayoutGuide.rightAnchor.constraint(equalTo: rightAnchor)
visibleContentLayoutGuideRight.priority = .defaultHigh
visibleContentLayoutGuideRight.isActive = true
visibleContentLayoutGuideBottom = visibleContentLayoutGuide.bottomAnchor.constraint(equalTo: bottomAnchor)
visibleContentLayoutGuideBottom.priority = .defaultHigh
visibleContentLayoutGuideBottom.isActive = true
}
private func setupLayout(contentView view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
contentViewTopEqualSuper = view.topAnchor.constraint(equalTo: contentWrapperView.topAnchor)
contentViewTopEqualSuper?.constant = contentInsets.top
contentViewTopEqualSuper?.isActive = alignContentToBottom == false
contentViewTopGreaterThanSuper = view.topAnchor.constraint(greaterThanOrEqualTo: contentWrapperView.topAnchor)
contentViewTopGreaterThanSuper?.constant = contentInsets.top
contentViewTopGreaterThanSuper?.isActive = alignContentToBottom == true
contentViewLeft = view.leftAnchor.constraint(equalTo: contentWrapperView.leftAnchor)
contentViewLeft?.constant = contentInsets.left
contentViewLeft?.isActive = true
contentViewRight = view.rightAnchor.constraint(equalTo: contentWrapperView.rightAnchor)
contentViewRight?.constant = -contentInsets.right
contentViewRight?.isActive = true
contentViewBottom = view.bottomAnchor.constraint(equalTo: contentWrapperView.bottomAnchor)
contentViewBottom?.constant = -contentInsets.bottom
contentViewBottom?.isActive = true
}
}

View File

@ -128,26 +128,3 @@ class UserNameView: UIControl {
}.store(in: &subscriptions)
}
}
class TextField: UITextField {
var textPadding = UIEdgeInsets(
top: 10,
left: 20,
bottom: 10,
right: 20
)
override func textRect(forBounds bounds: CGRect) -> CGRect {
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
let rect = super.textRect(forBounds: bounds)
return rect.inset(by: textPadding)
}
override func editingRect(forBounds bounds: CGRect) -> CGRect {
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
let rect = super.editingRect(forBounds: bounds)
return rect.inset(by: textPadding)
}
}