Merge branch 'feature/Notification' into 'develop'

Adding Notification component

See merge request BPHV_MIPS/vds_ios!53
This commit is contained in:
Bruce, Matt R 2023-04-04 17:03:44 +00:00
commit 2902a85c0c
15 changed files with 217 additions and 101 deletions

View File

@ -8,8 +8,6 @@
/* Begin PBXBuildFile section */
445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; };
44604AD029CE17EC00E62B51 /* NotificationTitleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604ACF29CE17EC00E62B51 /* NotificationTitleModel.swift */; };
44604AD229CE180F00E62B51 /* NotificationSubTitleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD129CE180F00E62B51 /* NotificationSubTitleModel.swift */; };
44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; };
44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; };
5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; };
@ -94,6 +92,7 @@
EAC9258F2911C9DE00091998 /* EntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC9258B2911C9DE00091998 /* EntryField.swift */; };
EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */; };
EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9829D4850E00101452 /* Clickable.swift */; };
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF1FE9A29DB1A6000101452 /* Changeable.swift */; };
EAF7F0952899861000B287F5 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0932899861000B287F5 /* Checkbox.swift */; };
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0992899B17200B287F5 /* CATransaction.swift */; };
EAF7F09E289AAEC000B287F5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F09D289AAEC000B287F5 /* Constants.swift */; };
@ -125,8 +124,6 @@
/* Begin PBXFileReference section */
445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
44604ACF29CE17EC00E62B51 /* NotificationTitleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTitleModel.swift; sourceTree = "<group>"; };
44604AD129CE180F00E62B51 /* NotificationSubTitleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSubTitleModel.swift; sourceTree = "<group>"; };
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = "<group>"; };
44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = "<group>"; };
@ -212,6 +209,7 @@
EAC9258B2911C9DE00091998 /* EntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = "<group>"; };
EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = "<group>"; };
EAF1FE9829D4850E00101452 /* Clickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clickable.swift; sourceTree = "<group>"; };
EAF1FE9A29DB1A6000101452 /* Changeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Changeable.swift; sourceTree = "<group>"; };
EAF7F0932899861000B287F5 /* Checkbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
EAF7F0992899B17200B287F5 /* CATransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CATransaction.swift; sourceTree = "<group>"; };
EAF7F09D289AAEC000B287F5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
@ -257,8 +255,6 @@
isa = PBXGroup;
children = (
445BA07729C07B3D0036A7C5 /* Notification.swift */,
44604ACF29CE17EC00E62B51 /* NotificationTitleModel.swift */,
44604AD129CE180F00E62B51 /* NotificationSubTitleModel.swift */,
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */,
);
path = Notification;
@ -418,6 +414,7 @@
isa = PBXGroup;
children = (
EA4DB2FC28D3D0CA00103EE3 /* AnyEquatable.swift */,
EAF1FE9A29DB1A6000101452 /* Changeable.swift */,
EAF1FE9829D4850E00101452 /* Clickable.swift */,
EAA5EEDF28F49DB3003B3210 /* Colorable.swift */,
EA3361A9288B25E40071C351 /* Disabling.swift */,
@ -792,7 +789,6 @@
EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */,
EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */,
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */,
44604AD029CE17EC00E62B51 /* NotificationTitleModel.swift in Sources */,
EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */,
EA985BF7296C665E00F2FF2E /* IconName.swift in Sources */,
EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */,
@ -825,10 +821,10 @@
EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */,
EAB1D2CF28ABEF2B00DAE764 /* Typography.swift in Sources */,
EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */,
EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */,
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */,
EAF1FE9929D4850E00101452 /* Clickable.swift in Sources */,
44604AD229CE180F00E62B51 /* NotificationSubTitleModel.swift in Sources */,
EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */,
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */,
44604AD729CE196600E62B51 /* Line.swift in Sources */,

View File

@ -7,14 +7,23 @@
import Foundation
import UIKit
import Combine
public class SelectorGroupHandlerBase<HandlerType: Control>: Control {
public class SelectorGroupHandlerBase<HandlerType: Control>: Control, Changeable {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var selectorViews: [HandlerType] = []
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------

View File

@ -121,7 +121,7 @@ open class ButtonBase: UIButton, Buttonable, Handlerable, ViewProtocol, Resettab
translatesAutoresizingMaskIntoConstraints = false
accessibilityCustomActions = []
setup()
setupDidChangeEvent()
setupDidChangeEvent(true)
updateView()
}
}

View File

@ -7,13 +7,13 @@
import Foundation
import UIKit
import Combine
import VDSColorTokens
import VDSFormControlsTokens
import Combine
/// Checkboxes are a multi-select component through which a customer indicates a choice. If a binary choice, the component is a checkbox. If the choice has multiple options, the component is a ``CheckboxGroup``.
@objc(VDSCheckboxBase)
open class Checkbox: Control, Errorable {
open class Checkbox: Control, Errorable, Changeable {
//--------------------------------------------------
// MARK: - Initializers
@ -63,6 +63,14 @@ open class Checkbox: Control, Errorable {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
open var label = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left

View File

@ -94,7 +94,7 @@ public class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable
accessibilityCustomActions = []
accessibilityTraits = .staticText
setup()
setupDidChangeEvent()
setupDidChangeEvent(true)
updateView()
}
}

View File

@ -18,7 +18,7 @@ public class Notification: View {
// MARK: - Enums
//--------------------------------------------------
public enum NotificationStyle: String, CaseIterable {
public enum Style: String, CaseIterable {
case info, success, warning, error
func styleIconName() -> Icon.Name {
@ -35,6 +35,10 @@ public class Notification: View {
}
}
public enum Layout: String, CaseIterable {
case vertical, horizontal
}
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
@ -52,6 +56,14 @@ public class Notification: View {
$0.axis = .vertical
}
private var labelButtonView = UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.alignment = .top
$0.distribution = .fillEqually
$0.axis = .vertical
$0.spacing = VDSLayout.Spacing.space2X.value
}
private var edgeSpacing: CGFloat {
return UIDevice.isIPad ? VDSLayout.Spacing.space5X.value : VDSLayout.Spacing.space4X.value
}
@ -64,24 +76,40 @@ public class Notification: View {
return UIDevice.isIPad ? VDSLayout.Spacing.space5X.value : VDSLayout.Spacing.space4X.value
}
private var minViewWidth: CGFloat {
return fullBleed ? 320 : 288
}
///Max view width is for Tablet
private var maxViewWidth: CGFloat {
return fullBleed ? 1272 : 1232
}
private var maxWidthConstraint: NSLayoutConstraint?
open var leadingConstraint: NSLayoutConstraint?
open var trailingConstraint: NSLayoutConstraint?
//--------------------------------------------------
// MARK: - View Properties
//--------------------------------------------------
open var typeIcon = Icon().with {
$0.name = .infoBold
$0.size = UIDevice.isIPad ? .medium : .small
}
open var closeButton = Icon().with {
$0.name = .close
$0.size = UIDevice.isIPad ? .medium : .small
}
open var titleLabel = Label().with {
$0.textStyle = .boldBodyLarge
$0.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall
}
open var subTitleLabel = Label().with {
$0.textStyle = .bodyLarge
$0.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall
}
open var buttonsView = ButtonGroup().with {
@ -89,11 +117,10 @@ public class Notification: View {
}
//Text
open var titleText: String = "" { didSet{didChange()}}
open var title: String = "" { didSet{didChange()}}
open var subTitleText: String? { didSet{didChange()}}
open var subTitle: String? { didSet{didChange()}}
#warning("will need to think about this one, probably create a model that has 2 props - text, onClick = (Button) -> () so we are not accessing the button directly. The only reason why I leave it open is for things like accessibility, but not for setting properties outside of this class. More or less follow how Tilelet is working, look at that, below is a temp fix until we can discuss with the guys")
//Buttons
open var primaryButtonModel: ButtonModel? { didSet{didChange()}}
open var primaryButton = Button().with {
@ -122,16 +149,31 @@ public class Notification: View {
}
internal var onCloseSubscriber: AnyCancellable?
//--------------------------------------------------
// MARK: - Modal Properties
// MARK: - Properties
//--------------------------------------------------
open var type: NotificationStyle = .info { didSet{didChange()}}
open var hideCloseButton: Bool = false { didSet{didChange()}}
open var type: Style = .info { didSet{didChange()}}
open var fullBleed: Bool = false { didSet {didChange()}}
var _layout: Layout = .vertical
open var layout: Layout {
set {
if !UIDevice.isIPad, newValue == .horizontal { return }
_layout = newValue
buttonsView.buttonPosition = _layout == .horizontal ? .center : .left
didChange()
}
get { _layout }
}
//--------------------------------------------------
// MARK: - Configuration
//--------------------------------------------------
private var backgroundColorConfiguration: AnyColorable = {
let config = KeyedColorConfiguration<Notification, NotificationStyle>(keyPath: \.type)
let config = KeyedColorConfiguration<Notification, Style>(keyPath: \.type)
config.setSurfaceColors(VDSColor.feedbackInformationBackgroundOnlight, VDSColor.feedbackInformationBackgroundOndark, forKey: .info)
config.setSurfaceColors(VDSColor.feedbackWarningBackgroundOnlight, VDSColor.feedbackWarningBackgroundOndark, forKey: .warning)
config.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forKey: .success)
@ -170,11 +212,15 @@ public class Notification: View {
NSLayoutConstraint.activate([
heightAnchor.constraint(greaterThanOrEqualToConstant: minViewHeight),
mainStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: minContentHeight)
mainStackView.heightAnchor.constraint(greaterThanOrEqualToConstant: minContentHeight),
widthAnchor.constraint(greaterThanOrEqualToConstant: minViewWidth)
])
maxWidthConstraint = widthAnchor.constraint(lessThanOrEqualToConstant: maxViewWidth)
labelButtonView.addArrangedSubview(labelsView)
mainStackView.addArrangedSubview(typeIcon)
mainStackView.addArrangedSubview(labelsView)
mainStackView.addArrangedSubview(labelButtonView)
mainStackView.addArrangedSubview(closeButton)
//labels
@ -186,14 +232,29 @@ public class Notification: View {
super.reset()
titleLabel.reset()
titleLabel.text = ""
titleLabel.textStyle = UIDevice.isIPad ? .boldBodyLarge : .boldBodySmall
subTitleLabel.reset()
subTitleLabel.textStyle = UIDevice.isIPad ? .bodyLarge : .bodySmall
buttonsView.reset()
primaryButton.reset()
secondaryButton.reset()
buttonsView.buttonPosition = .left
primaryButtonModel = nil
secondaryButtonModel = nil
type = .info
typeIcon.size = UIDevice.isIPad ? .medium : .small
typeIcon.name = .infoBold
onCloseClick = nil
closeButton.size = UIDevice.isIPad ? .medium : .small
closeButton.name = .close
layout = .vertical
hideCloseButton = false
fullBleed = false
}
//--------------------------------------------------
@ -204,6 +265,7 @@ public class Notification: View {
updateIcons()
updateLabels()
updateButtons()
setConstraints()
}
private func updateIcons() {
@ -211,21 +273,22 @@ public class Notification: View {
typeIcon.name = type.styleIconName()
typeIcon.color = iconColor
closeButton.color = iconColor
closeButton.isHidden = hideCloseButton
}
private func updateLabels() {
titleLabel.surface = surface
subTitleLabel.surface = surface
if !titleText.isEmpty {
titleLabel.text = titleText
if !title.isEmpty {
titleLabel.text = title
labelsView.addArrangedSubview(titleLabel)
} else {
titleLabel.removeFromSuperview()
}
if let subTitleText {
subTitleLabel.text = subTitleText
if let subTitle {
subTitleLabel.text = subTitle
labelsView.addArrangedSubview(subTitleLabel)
} else {
subTitleLabel.removeFromSuperview()
@ -254,14 +317,32 @@ public class Notification: View {
buttonsView.removeFromSuperview()
} else {
labelsView.setCustomSpacing(VDSLayout.Spacing.space3X.value, after: subTitleLabel)
///This below doesn't work
buttonsView.buttons = buttons
labelsView.addArrangedSubview(buttonsView)
labelButtonView.axis = layout == .vertical ? .vertical : .horizontal
labelButtonView.addArrangedSubview(buttonsView)
buttonsView
.pinLeading()
.pinTrailing()
}
}
private func setConstraints() {
maxWidthConstraint?.constant = maxViewWidth
maxWidthConstraint?.isActive = UIDevice.isIPad
if leadingConstraint == nil, let superview {
leadingConstraint = NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: superview, attribute: .leading, multiplier: 1, constant: 0)
}
if trailingConstraint == nil, let superview {
trailingConstraint = NSLayoutConstraint(item: superview, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0)
}
leadingConstraint?.isActive = fullBleed
trailingConstraint?.isActive = fullBleed
}
}

View File

@ -1,26 +0,0 @@
//
// NotificationSubTitleModel.swift
// VDS
//
// Created by Nadigadda, Sumanth on 24/03/23.
//
import Foundation
extension Notification {
public struct SubTitleModel {
public var text: String
public var textAttributes: [any LabelAttributeModel]?
public var textStyle: TextStyle = .bodySmall
public var numberOfLines: Int
public init(text: String,
textColor: Use = .primary,
textAttributes: [any LabelAttributeModel]? = nil,
numberOfLines: Int = 0) {
self.text = text
self.textAttributes = textAttributes
self.numberOfLines = numberOfLines
}
}
}

View File

@ -1,25 +0,0 @@
//
// NotificationTitleModel.swift
// VDS
//
// Created by Nadigadda, Sumanth on 24/03/23.
//
import Foundation
extension Notification {
public struct TitleModel {
public var text: String
public var textAttributes: [any LabelAttributeModel]?
public var textStyle: TextStyle = .boldBodySmall
public var numberOfLines: Int
public init(text: String,
textAttributes: [any LabelAttributeModel]? = nil,
numberOfLines: Int = 0) {
self.text = text
self.textAttributes = textAttributes
self.numberOfLines = numberOfLines
}
}
}

View File

@ -7,12 +7,12 @@
import Foundation
import UIKit
import Combine
import VDSColorTokens
import VDSFormControlsTokens
import Combine
@objc(VDSRadioBox)
open class RadioBox: Control {
open class RadioBox: Control, Changeable {
//--------------------------------------------------
// MARK: - Initializers
@ -56,6 +56,14 @@ open class RadioBox: Control {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
open var textLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left

View File

@ -7,11 +7,12 @@
import Foundation
import UIKit
import Combine
import VDSColorTokens
import VDSFormControlsTokens
@objc(VDSRadioButton)
open class RadioButton: Control, Errorable {
open class RadioButton: Control, Errorable, Changeable {
//--------------------------------------------------
// MARK: - Initializers
@ -61,6 +62,14 @@ open class RadioButton: Control, Errorable {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
open var label = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textPosition = .left

View File

@ -12,7 +12,7 @@ import VDSFormControlsTokens
import Combine
@objc(VDSEntryField)
open class EntryField: Control {
open class EntryField: Control, Changeable {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
@ -98,6 +98,14 @@ open class EntryField: Control {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
open var titleLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.attributes = []

View File

@ -18,7 +18,7 @@ import Combine
Knob: The circular indicator that slides on the container.
*/
@objc(VDSToggle)
open class Toggle: Control {
open class Toggle: Control, Changeable {
//--------------------------------------------------
// MARK: - Enums
//--------------------------------------------------
@ -107,6 +107,14 @@ open class Toggle: Control {
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
public var onChangeSubscriber: AnyCancellable? {
willSet {
if let onChangeSubscriber {
onChangeSubscriber.cancel()
}
}
}
open var label = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
}

View File

@ -0,0 +1,31 @@
//
// Changeable.swift
// VDS
//
// Created by Matt Bruce on 4/3/23.
//
import Foundation
import UIKit
import Combine
public protocol Changeable: Handlerable where Self: UIControl {
var onChangeSubscriber: AnyCancellable? { get set }
}
extension Changeable {
public var onChange: ((Self) -> ())? {
get { return nil }
set {
if let newValue {
onChangeSubscriber = publisher(for: .valueChanged)
.sink { c in
newValue(c)
}
} else {
onChangeSubscriber?.cancel()
onChangeSubscriber = nil
}
}
}
}

View File

@ -15,13 +15,6 @@ public protocol Clickable: Handlerable where Self: UIControl {
}
extension Clickable {
public func addEvent(event: UIControl.Event, block: @escaping (Self)->()) {
publisher(for: event)
.sink(receiveValue: { c in
block(c)
}).store(in: &subscribers)
}
public var onClick: ((Self) -> ())? {
get { return nil }
set {

View File

@ -17,15 +17,22 @@ public protocol Handlerable: AnyObject, Initable, Disabling, Surfaceable {
extension Handlerable {
public func setupDidChangeEvent() {
handlerPublisher().sink { [weak self] _ in
self?.updateView()
public func setupDidChangeEvent(_ debounce: Bool = false) {
handlerPublisher(debounce)
.sink { [weak self] _ in
self?.updateView()
}.store(in: &subscribers)
}
public func handlerPublisher() -> AnyPublisher<Void, Never> {
subject
.eraseToAnyPublisher()
public func handlerPublisher(_ debounce: Bool = false) -> AnyPublisher<Void, Never> {
if debounce {
return subject
.debounce(for: .seconds(Constants.StateDebounce), scheduler: RunLoop.main)
.eraseToAnyPublisher()
} else {
return subject
.eraseToAnyPublisher()
}
}
}
@ -34,3 +41,12 @@ extension Handlerable where Self: UIView {
subject.send()
}
}
extension Handlerable where Self: UIControl {
public func addEvent(event: UIControl.Event, block: @escaping (Self)->()) {
publisher(for: event)
.sink(receiveValue: { c in
block(c)
}).store(in: &subscribers)
}
}