diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 089a1176..0244a9b3 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; }; 5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; }; + EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */; }; EA1F266528B945070033E859 /* RadioSwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266128B945070033E859 /* RadioSwatch.swift */; }; EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1F266228B945070033E859 /* RadioSwatchGroup.swift */; }; EA336171288B19200071C351 /* VDS.docc in Sources */ = {isa = PBXBuildFile; fileRef = EA336170288B19200071C351 /* VDS.docc */; }; @@ -57,6 +58,9 @@ EAB1D2CF28ABEF2B00DAE764 /* Typography.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */; }; EAB1D2E628AE842000DAE764 /* Publisher+Bind.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2E328AE842000DAE764 /* Publisher+Bind.swift */; }; EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */; }; + EAB5FED429267EB300998C17 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FED329267EB300998C17 /* UIView.swift */; }; + EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */; }; + EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */; }; EAC9257D29119B5400091998 /* TextLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC9257C29119B5400091998 /* TextLink.swift */; }; EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC925822911B35300091998 /* TextLinkCaret.swift */; }; EAC925842911C63100091998 /* Colorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEDF28F49DB3003B3210 /* Colorable.swift */; }; @@ -70,7 +74,7 @@ EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A1289AFB3900B287F5 /* Errorable.swift */; }; EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A3289B017C00B287F5 /* LabelAttributeModel.swift */; }; EAF7F0A6289B0CE000B287F5 /* Resetable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0A5289B0CE000B287F5 /* Resetable.swift */; }; - EAF7F0AB289B13FD00B287F5 /* FontLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AA289B13FD00B287F5 /* FontLabelAttribute.swift */; }; + EAF7F0AB289B13FD00B287F5 /* TypographicalStyleLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AA289B13FD00B287F5 /* TypographicalStyleLabelAttribute.swift */; }; EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AC289B142900B287F5 /* StrikeThroughLabelAttribute.swift */; }; EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0AE289B144C00B287F5 /* UnderlineLabelAttribute.swift */; }; EAF7F0B1289B177F00B287F5 /* ColorLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F0B0289B177F00B287F5 /* ColorLabelAttribute.swift */; }; @@ -95,6 +99,7 @@ /* Begin PBXFileReference section */ 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = ""; }; 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; + EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = ""; }; EA1F266128B945070033E859 /* RadioSwatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatch.swift; sourceTree = ""; }; EA1F266228B945070033E859 /* RadioSwatchGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioSwatchGroup.swift; sourceTree = ""; }; EA33616C288B19200071C351 /* VDS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VDS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -145,6 +150,9 @@ EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = ""; }; EAB1D2E328AE842000DAE764 /* Publisher+Bind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Publisher+Bind.swift"; sourceTree = ""; }; EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControlPublisher.swift; sourceTree = ""; }; + EAB5FED329267EB300998C17 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; + EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupPositionLayout.swift; sourceTree = ""; }; + EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfSizingCollectionView.swift; sourceTree = ""; }; EAC9257C29119B5400091998 /* TextLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLink.swift; sourceTree = ""; }; EAC925822911B35300091998 /* TextLinkCaret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLinkCaret.swift; sourceTree = ""; }; EAC925872911C9DE00091998 /* TextEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = ""; }; @@ -157,7 +165,7 @@ EAF7F0A1289AFB3900B287F5 /* Errorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errorable.swift; sourceTree = ""; }; EAF7F0A3289B017C00B287F5 /* LabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeModel.swift; sourceTree = ""; }; EAF7F0A5289B0CE000B287F5 /* Resetable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resetable.swift; sourceTree = ""; }; - EAF7F0AA289B13FD00B287F5 /* FontLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontLabelAttribute.swift; sourceTree = ""; }; + EAF7F0AA289B13FD00B287F5 /* TypographicalStyleLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypographicalStyleLabelAttribute.swift; sourceTree = ""; }; EAF7F0AC289B142900B287F5 /* StrikeThroughLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrikeThroughLabelAttribute.swift; sourceTree = ""; }; EAF7F0AE289B144C00B287F5 /* UnderlineLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlineLabelAttribute.swift; sourceTree = ""; }; EAF7F0B0289B177F00B287F5 /* ColorLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorLabelAttribute.swift; sourceTree = ""; }; @@ -202,6 +210,7 @@ EA0FC2BE2912D18200DF80B4 /* Buttons */ = { isa = PBXGroup; children = ( + EA0FC2C42914221800DF80B4 /* ButtonGroup */, 5FC35BE128D513EB004EBEAC /* Button */, EAC9257E29119B5D00091998 /* TextLink */, EAC925812911B34300091998 /* TextLinkCaret */, @@ -209,6 +218,15 @@ path = Buttons; sourceTree = ""; }; + EA0FC2C42914221800DF80B4 /* ButtonGroup */ = { + isa = PBXGroup; + children = ( + EA0FC2C52914222900DF80B4 /* ButtonGroup.swift */, + EAB5FEEC2927E1B200998C17 /* ButtonGroupPositionLayout.swift */, + ); + path = ButtonGroup; + sourceTree = ""; + }; EA1F265F28B945070033E859 /* RadioSwatch */ = { isa = PBXGroup; children = ( @@ -306,6 +324,7 @@ EA33623D2892EE950071C351 /* UIDevice.swift */, EAF7F0B4289C126F00B287F5 /* UILabel.swift */, EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */, + EAB5FED329267EB300998C17 /* UIView.swift */, ); path = Extensions; sourceTree = ""; @@ -339,6 +358,7 @@ EA3361B5288B2A410071C351 /* Control.swift */, EAF7F09F289AB7EC00B287F5 /* View.swift */, EA4DB18428CA967F00103EE3 /* SelectorGroupHandlerBase.swift */, + EAB5FEF02927F4AA00998C17 /* SelfSizingCollectionView.swift */, ); path = Classes; sourceTree = ""; @@ -485,7 +505,7 @@ EA978EC4291D6AFE00ACC883 /* AnyLabelAttribute.swift */, EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */, EAF7F0B0289B177F00B287F5 /* ColorLabelAttribute.swift */, - EAF7F0AA289B13FD00B287F5 /* FontLabelAttribute.swift */, + EAF7F0AA289B13FD00B287F5 /* TypographicalStyleLabelAttribute.swift */, EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */, EAF7F0AC289B142900B287F5 /* StrikeThroughLabelAttribute.swift */, EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */, @@ -640,6 +660,7 @@ EAB1D2CD28ABE76100DAE764 /* Withable.swift in Sources */, EAF7F0952899861000B287F5 /* Checkbox.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, + EAB5FEED2927E1B200998C17 /* ButtonGroupPositionLayout.swift in Sources */, EA4DB30228DCBCA500103EE3 /* Badge.swift in Sources */, EA33624728931B050071C351 /* Initable.swift in Sources */, EAF7F0A4289B017C00B287F5 /* LabelAttributeModel.swift in Sources */, @@ -647,6 +668,7 @@ EAC9258F2911C9DE00091998 /* EntryField.swift in Sources */, EAB1D2EA28AE84AA00DAE764 /* UIControlPublisher.swift in Sources */, EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */, + EA0FC2C62914222900DF80B4 /* ButtonGroup.swift in Sources */, EA89200628B526D6006B9984 /* CheckboxGroup.swift in Sources */, EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */, EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */, @@ -660,7 +682,7 @@ EA1F266528B945070033E859 /* RadioSwatch.swift in Sources */, EA4DB18528CA967F00103EE3 /* SelectorGroupHandlerBase.swift in Sources */, EA89200228AECF2A006B9984 /* UIButton+Publisher.swift in Sources */, - EAF7F0AB289B13FD00B287F5 /* FontLabelAttribute.swift in Sources */, + EAF7F0AB289B13FD00B287F5 /* TypographicalStyleLabelAttribute.swift in Sources */, EAB1D29C28A5618900DAE764 /* RadioButtonGroup.swift in Sources */, EA336171288B19200071C351 /* VDS.docc in Sources */, EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */, @@ -669,10 +691,12 @@ EA3361B6288B2A410071C351 /* Control.swift in Sources */, 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */, EAF7F0B7289C12A600B287F5 /* UITapGestureRecognizer.swift in Sources */, + EAB5FED429267EB300998C17 /* UIView.swift in Sources */, EA3361AD288B26190071C351 /* DataTrackable.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA3362302891EB4A0071C351 /* Fonts.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, + EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, EA3361BF288B2EA60071C351 /* Handlerable.swift in Sources */, EA3361A8288B23300071C351 /* UIColor.swift in Sources */, diff --git a/VDS/Classes/Control.swift b/VDS/Classes/Control.swift index 12e8fba4..bd37b435 100644 --- a/VDS/Classes/Control.swift +++ b/VDS/Classes/Control.swift @@ -92,6 +92,7 @@ open class Control: UIControl, Handlerable, ViewProtocol, Resettable { // MARK: - ViewProtocol /// Will be called only once. open func setup() { + backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false insetsLayoutMarginsFromSafeArea = false } diff --git a/VDS/Classes/SelfSizingCollectionView.swift b/VDS/Classes/SelfSizingCollectionView.swift new file mode 100644 index 00000000..69270d9a --- /dev/null +++ b/VDS/Classes/SelfSizingCollectionView.swift @@ -0,0 +1,72 @@ +// +// SelfSizingCollectionView.swift +// VDS +// +// Created by Matt Bruce on 11/18/22. +// + +import Foundation +import UIKit + +public final class SelfSizingCollectionView: UICollectionView { + + private var contentSizeObservation: NSKeyValueObservation? + + // MARK: - Lifecycle + + public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: layout) + self.setupContentSizeObservation() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + self.setupContentSizeObservation() + } + + // MARK: - UIView + + public override var intrinsicContentSize: CGSize { + let contentSize = self.contentSize + //print(#function, contentSize) + return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) + } + + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + //print(type(of: self), #function) + super.traitCollectionDidChange(previousTraitCollection) + + // We need to handle any change that will affect layout and/or anything that affects size of a UILabel + if self.traitCollection.hasDifferentTextAppearance(comparedTo: previousTraitCollection) { + self.collectionViewLayout.invalidateLayout() + } + } + + public override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { + let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) + //print(type(of: self), #function, targetSize, "->", size) + return size + } + + // MARK: - Private + + private func setupContentSizeObservation() { + // Observing the value of contentSize seems to be the only reliable way to get the contentSize after the collection view lays out its subviews. + self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in + // If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`. + if change.newValue != change.oldValue { + self?.invalidateIntrinsicContentSize() + } + } + } +} + +extension UITraitCollection { + + public func hasDifferentTextAppearance(comparedTo traitCollection: UITraitCollection?) -> Bool { + var result = self.preferredContentSizeCategory != traitCollection?.preferredContentSizeCategory + result = result || self.legibilityWeight != traitCollection?.legibilityWeight + return result + } +} + diff --git a/VDS/Classes/View.swift b/VDS/Classes/View.swift index 837d81a8..1671b377 100644 --- a/VDS/Classes/View.swift +++ b/VDS/Classes/View.swift @@ -84,6 +84,7 @@ open class View: UIView, Handlerable, ViewProtocol, Resettable { // MARK: - ViewProtocol /// Will be called only once. open func setup() { + backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false insetsLayoutMarginsFromSafeArea = false } diff --git a/VDS/Components/Badge/Badge.swift b/VDS/Components/Badge/Badge.swift index 288ebf9b..e436bbf4 100644 --- a/VDS/Components/Badge/Badge.swift +++ b/VDS/Components/Badge/Badge.swift @@ -18,6 +18,11 @@ public enum BadgeFillColor: String, Codable, CaseIterable { @objc(VDSBadge) public class Badge: View, Accessable { + private var contentView = UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.layer.cornerRadius = 2 + } + private var label = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.adjustsFontSizeToFitWidth = false @@ -65,14 +70,18 @@ public class Badge: View, Accessable { isAccessibilityElement = true accessibilityTraits = .staticText - addSubview(label) - - layer.cornerRadius = 2 - label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4).isActive = true - label.topAnchor.constraint(equalTo: topAnchor, constant: 2).isActive = true - trailingAnchor.constraint(greaterThanOrEqualTo: label.trailingAnchor, constant: 4).isActive = true - bottomAnchor.constraint(greaterThanOrEqualTo: label.bottomAnchor, constant: 2).isActive = true + contentView.addSubview(label) + addSubview(contentView) + + contentView + .pinTop() + .pinBottom() + .pinLeading() + + contentView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor).isActive = true + + label.pinToSuperView(.init(top: 2, left: 4, bottom: 2, right: 4)) maxWidthConstraint = label.widthAnchor.constraint(lessThanOrEqualToConstant: 100) minWidthConstraint = label.widthAnchor.constraint(greaterThanOrEqualToConstant: 23) @@ -174,13 +183,12 @@ public class Badge: View, Accessable { }.eraseToAnyColorable() } } - //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { - backgroundColor = backgroundColor() + contentView.backgroundColor = backgroundColor() label.textColorConfiguration = textColorConfiguration() label.numberOfLines = numberOfLines diff --git a/VDS/Components/Buttons/Button/Button.swift b/VDS/Components/Buttons/Button/Button.swift index 4a065e3e..89a9440b 100644 --- a/VDS/Components/Buttons/Button/Button.swift +++ b/VDS/Components/Buttons/Button/Button.swift @@ -11,13 +11,19 @@ import VDSColorTokens import VDSFormControlsTokens import Combine +public protocol Buttonable: UIControl, Surfaceable, Disabling { + var availableSizes: [ButtonSize] { get } + var text: String? { get set } + var intrinsicContentSize: CGSize { get } +} + public enum ButtonSize: String, Codable, CaseIterable { case large case small } @objc(VDSButton) -open class Button: UIButton, Handlerable, ViewProtocol, Resettable, Useable { +open class Button: UIButton, Buttonable, Handlerable, ViewProtocol, Resettable, Useable { //-------------------------------------------------- // MARK: - Combine Properties @@ -36,6 +42,8 @@ open class Button: UIButton, Handlerable, ViewProtocol, Resettable, Useable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + public var availableSizes: [ButtonSize] { [.large, .small] } + open var text: String? { didSet { didChange() } } open var use: Use = .primary { didSet { didChange() }} @@ -159,6 +167,15 @@ open class Button: UIButton, Handlerable, ViewProtocol, Resettable, Useable { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- + override open var intrinsicContentSize: CGSize { + let intrinsicContentSize = super.intrinsicContentSize + + let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right + let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom + + return CGSize(width: adjustedWidth, height: adjustedHeight) + } + open func updateView() { let bgColor = buttonBackgroundColorConfiguration.getColor(self) diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift new file mode 100644 index 00000000..12d08bf7 --- /dev/null +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift @@ -0,0 +1,168 @@ +// +// ButtonGroup.swift +// VDS +// +// Created by Matt Bruce on 11/3/22. +// + +import Foundation +import UIKit +import VDSColorTokens +import VDSFormControlsTokens +import Combine + +@objc(VDSButtonGroup) +open class ButtonGroup: View, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, ButtongGroupPositionLayoutDelegate { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + //An object containing number of Button components per row, in each viewport + open var rowQuantityPhone: Int = 0 { didSet { didChange() } } + + open var rowQuantityTablet: Int = 0 { didSet { didChange() } } + + public var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone } + + //If provided, aligns TextLink/TextLinkCaret alignment when rowQuantity is set one. + open var buttonPosition: ButtonPosition = .center { didSet { didChange() }} + + open var buttons: [Buttonable] = [] { didSet { didChange() }} + + //If provided, width of Button components will be rendered based on this value. If omitted, default button widths are rendered. + open var buttonWidth: CGFloat? { + didSet { + buttons.forEach { button in + if let button = button as? Button { + button.width = buttonWidth + } + } + didChange() + } + } + + //-------------------------------------------------- + // MARK: - Private Properties + //-------------------------------------------------- + private let lineSpacing: CGFloat = 12.0 + private let itemSpacing: CGFloat = 16.0 + private let estimatedCellHeight: CGFloat = 40.0 + private let estimatedCellWidth: CGFloat = 150.0 + + fileprivate lazy var positionLayout = ButtonGroupPositionLayout().with { + $0.position = .center + $0.delegate = self + } + + fileprivate lazy var collectionView: SelfSizingCollectionView = { + + return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with { + $0.backgroundColor = .clear + $0.showsHorizontalScrollIndicator = false + $0.showsVerticalScrollIndicator = false + $0.isScrollEnabled = false + $0.translatesAutoresizingMaskIntoConstraints = false + $0.dataSource = self + $0.delegate = self + $0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell") + } + }() + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + override public var disabled: Bool { + didSet { + buttons.forEach { button in + button.disabled = disabled + } + } + } + + override public var surface: Surface { + didSet { + buttons.forEach { button in + button.surface = surface + } + } + } + + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + required public init() { + super.init(frame: .zero) + initialSetup() + } + + public override init(frame: CGRect) { + super.init(frame: .zero) + initialSetup() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + initialSetup() + } + //-------------------------------------------------- + // MARK: - Public Functions + //-------------------------------------------------- + open override func setup() { + super.setup() + isAccessibilityElement = true + accessibilityTraits = .button + addSubview(collectionView) + collectionView.pinToSuperView() + } + + //-------------------------------------------------- + // MARK: - Overrides + //-------------------------------------------------- + open override func updateView() { + super.updateView() + positionLayout.position = buttonPosition + collectionView.reloadData() + } + + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any collection size changes + DispatchQueue.main.async { + self.collectionView.collectionViewLayout.invalidateLayout() + } + } + + //-------------------------------------------------- + // MARK: - UICollectionViewDataSource + //-------------------------------------------------- + public func numberOfSections(in collectionView: UICollectionView) -> Int { + return 1 + } + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return buttons.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let button = buttons[indexPath.row] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) + cell.subviews.forEach { $0.removeFromSuperview() } + cell.subviews.forEach { $0.removeFromSuperview() } + cell.addSubview(button) + button.pinToSuperView() + return cell + } + + public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { + buttons[indexPath.row].intrinsicContentSize + } + + public func collectionView(_ collectionView: UICollectionView, insetsForItemsInSection section: Int) -> UIEdgeInsets { + UIEdgeInsets.zero + } + + public func collectionView(_ collectionView: UICollectionView, itemSpacingInSection section: Int) -> CGFloat { + itemSpacing + } +} diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift new file mode 100644 index 00000000..c2d6b74a --- /dev/null +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift @@ -0,0 +1,167 @@ +// +// ButtonGroupPositionLayout.swift +// VDS +// +// Created by Matt Bruce on 11/18/22. +// + +import Foundation +import UIKit + +class ButtonCollectionViewRow { + var attributes = [UICollectionViewLayoutAttributes]() + var spacing: CGFloat = 0 + + init(spacing: CGFloat) { + self.spacing = spacing + } + + func add(attribute: UICollectionViewLayoutAttributes) { + attributes.append(attribute) + } + + var rowWidth: CGFloat { + return attributes.reduce(0, { result, attribute -> CGFloat in + return result + attribute.frame.width + }) + CGFloat(attributes.count - 1) * spacing + } + + func layout(for position: ButtonPosition, with collectionViewWidth: CGFloat){ + var offset = 0.0 + + switch position { + case .left: + break + case .center: + offset = (collectionViewWidth - rowWidth) / 2 + case .right: + offset = (collectionViewWidth - rowWidth) + } + + for attribute in attributes { + attribute.frame.origin.x = offset + offset += attribute.frame.width + spacing + } + } +} + +public enum ButtonPosition: String, CaseIterable { + case left, center, right +} + +protocol ButtongGroupPositionLayoutDelegate: AnyObject { + func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize + func collectionView(_ collectionView: UICollectionView, insetsForItemsInSection section: Int) -> UIEdgeInsets + func collectionView(_ collectionView: UICollectionView, itemSpacingInSection section: Int) -> CGFloat +} + +class ButtonGroupPositionLayout: UICollectionViewLayout { + + weak var delegate: ButtongGroupPositionLayoutDelegate? + + // Total height of the content. Will be used to configure the scrollview content + var layoutHeight: CGFloat = 0.0 + var position: ButtonPosition = .left + private var itemCache: [UICollectionViewLayoutAttributes] = [] + + override func prepare() { + super.prepare() + + itemCache.removeAll() + + guard let collectionView = collectionView else { + return + } + var itemSpacing = 0.0 + // Variable to track the width of the layout at the current state when the item is being drawn + var layoutWidthIterator: CGFloat = 0.0 + + for section in 0.. collectionView.frame.width { + // If the current row width (after this item being laid out) is exceeding the width of the collection view content, put it in the next line + layoutWidthIterator = 0.0 + layoutHeight += itemSize.height + interItemSpacing + } + + let frame = CGRect(x: layoutWidthIterator + insets.left, y: layoutHeight, width: itemSize.width, height: itemSize.height) + let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) + attributes.frame = frame + itemCache.append(attributes) + layoutWidthIterator = layoutWidthIterator + frame.width + interItemSpacing + } + + layoutHeight += itemSize.height + insets.bottom + layoutWidthIterator = 0.0 + } + + //Turn into rows and re-calculate + var rows = [ButtonCollectionViewRow]() + var currentRowY: CGFloat = -1 + + for attribute in itemCache { + if currentRowY != attribute.frame.midY { + currentRowY = attribute.frame.midY + rows.append(ButtonCollectionViewRow(spacing: itemSpacing)) + } + rows.last?.add(attribute: attribute) + } + + //recalculate rows based off of positions + rows.forEach { $0.layout(for: position, with: collectionView.frame.width) } + let rowAttributes = rows.flatMap { $0.attributes } + itemCache = rowAttributes + + } + + override func layoutAttributesForElements(in rect: CGRect)-> [UICollectionViewLayoutAttributes]? { + super.layoutAttributesForElements(in: rect) + + var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = [] + + for attributes in itemCache { + if attributes.frame.intersects(rect) { + visibleLayoutAttributes.append(attributes) + } + } + + return visibleLayoutAttributes + } + + override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { + super.layoutAttributesForItem(at: indexPath) + return itemCache[indexPath.row] + } + + override var collectionViewContentSize: CGSize { + return CGSize(width: contentWidth, height: layoutHeight) + } + + private var contentWidth: CGFloat { + guard let collectionView = collectionView else { + return 0 + } + let insets = collectionView.contentInset + return collectionView.bounds.width - (insets.left + insets.right) + } + + override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { + layoutHeight = 0.0 + return true + } +} + diff --git a/VDS/Components/Buttons/TextLink/TextLink.swift b/VDS/Components/Buttons/TextLink/TextLink.swift index d1287c2e..f3e8a6f3 100644 --- a/VDS/Components/Buttons/TextLink/TextLink.swift +++ b/VDS/Components/Buttons/TextLink/TextLink.swift @@ -12,7 +12,7 @@ import VDSFormControlsTokens import Combine @objc(VDSTextLink) -open class TextLink: Control { +open class TextLink: Control, Buttonable { //-------------------------------------------------- // MARK: - Private Properties @@ -26,7 +26,9 @@ open class TextLink: Control { open var text: String? { didSet { didChange() } } open var size: ButtonSize = .large { didSet { didChange() }} - + + public var availableSizes: [ButtonSize] { [.large, .small] } + private var height: CGFloat { switch size { case .large: @@ -72,11 +74,8 @@ open class TextLink: Control { }.store(in: &subscribers) //pin stackview to edges - label.topAnchor.constraint(equalTo: topAnchor).isActive = true - label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - + label.pinToSuperView() + label.numberOfLines = 1 heightConstraint = heightAnchor.constraint(equalToConstant: height) heightConstraint?.isActive = true } @@ -94,6 +93,10 @@ open class TextLink: Control { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- + override open var intrinsicContentSize: CGSize { + return CGSize(width: label.intrinsicContentSize.width, height: height) + } + open override func updateView() { label.surface = surface label.disabled = disabled diff --git a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift index 83958072..577ff332 100644 --- a/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift +++ b/VDS/Components/Buttons/TextLinkCaret/TextLinkCaret.swift @@ -16,7 +16,7 @@ public enum TextLinkCaretPosition: String, CaseIterable { } @objc(VDSTextLinkCaret) -open class TextLinkCaret: Control { +open class TextLinkCaret: Control, Buttonable { //-------------------------------------------------- // MARK: - Private Properties @@ -35,6 +35,8 @@ open class TextLinkCaret: Control { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + public var availableSizes: [ButtonSize] { [.large] } + open var text: String? { didSet { didChange() } } open var iconPosition: TextLinkCaretPosition = .right { didSet { didChange() } } @@ -81,10 +83,10 @@ open class TextLinkCaret: Control { let size = caretView.size!.dimensions() caretView.frame = .init(x: 0, y: 0, width: size.width, height: size.height) addSubview(label) - label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - label.topAnchor.constraint(equalTo: topAnchor).isActive = true - label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + label.pinToSuperView() + + label.numberOfLines = 1 + } //-------------------------------------------------- @@ -109,6 +111,14 @@ open class TextLinkCaret: Control { //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- + override open var intrinsicContentSize: CGSize { + var itemWidth = label.intrinsicContentSize.width + if let caretWidth = caretView.size?.dimensions().width { + itemWidth += caretWidth + } + return CGSize(width: itemWidth, height: height) + } + open override func updateView() { let updatedText = text ?? "" @@ -121,9 +131,7 @@ open class TextLinkCaret: Control { let location = iconPosition == .right ? updatedText.count + 1 : 0 let textColor = label.textColorConfiguration.getColor(self) let imageAttribute = ImageLabelAttribute(location: location, - length: 1, image: image, - frame: .init(x: 0, y: 0, width: image.size.width, height: image.size.height), tintColor: textColor) label.surface = surface label.disabled = disabled diff --git a/VDS/Components/Checkbox/Checkbox.swift b/VDS/Components/Checkbox/Checkbox.swift index 3a515eb4..22ff909b 100644 --- a/VDS/Components/Checkbox/Checkbox.swift +++ b/VDS/Components/Checkbox/Checkbox.swift @@ -190,10 +190,7 @@ open class CheckboxBase: Control, Accessable, DataTrackable, BinaryColorable, Er updateSelector() - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + mainStackView.pinToSuperView() } diff --git a/VDS/Components/Checkbox/CheckboxGroup.swift b/VDS/Components/Checkbox/CheckboxGroup.swift index 7501d11f..e2e3b248 100644 --- a/VDS/Components/Checkbox/CheckboxGroup.swift +++ b/VDS/Components/Checkbox/CheckboxGroup.swift @@ -79,10 +79,7 @@ public class CheckboxGroupBase: SelectorGroupHandlerB accessibilityTraits = .button addSubview(mainStackView) - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + mainStackView.pinToSuperView() } public var selectedHandlers: [HandlerType]? { diff --git a/VDS/Components/Label/Attributes/ImageLabelAttribute.swift b/VDS/Components/Label/Attributes/ImageLabelAttribute.swift index ca62f602..bf6e067a 100644 --- a/VDS/Components/Label/Attributes/ImageLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/ImageLabelAttribute.swift @@ -16,11 +16,11 @@ public struct ImageLabelAttribute: AttachmentLabelAttributeModel { } public var id = UUID() public var location: Int - public var length: Int + public var length: Int = 1 public var imageName: String? public var image: UIImage? - public var frame: CGRect - public var tintColor: UIColor + public var frame: CGRect? + public var tintColor: UIColor? public static func == (lhs: ImageLabelAttribute, rhs: ImageLabelAttribute) -> Bool { lhs.isEqual(rhs) } @@ -31,8 +31,8 @@ public struct ImageLabelAttribute: AttachmentLabelAttributeModel { private func imageAttachment(image: UIImage) -> NSTextAttachment { let attachment = NSTextAttachment() - attachment.image = image.withTintColor(tintColor) - attachment.bounds = frame + attachment.image = tintColor != nil ? image.withTintColor(tintColor!) : image + attachment.bounds = frame ?? .init(x: 0, y: 0, width: image.size.width, height: image.size.height) return attachment } diff --git a/VDS/Components/Label/Attributes/LabelAttributeModel.swift b/VDS/Components/Label/Attributes/LabelAttributeModel.swift index 7196a346..0976fe84 100644 --- a/VDS/Components/Label/Attributes/LabelAttributeModel.swift +++ b/VDS/Components/Label/Attributes/LabelAttributeModel.swift @@ -44,6 +44,6 @@ public extension NSAttributedString { else { return AnyAttribute(location: range.location, length: range.length, key: key, value: value) } - return FontLabelAttribute(location: range.location, length: range.length, style: style) + return TypographicalStyleLabelAttribute(location: range.location, length: range.length, style: style) } } diff --git a/VDS/Components/Label/Attributes/FontLabelAttribute.swift b/VDS/Components/Label/Attributes/TypographicalStyleLabelAttribute.swift similarity index 95% rename from VDS/Components/Label/Attributes/FontLabelAttribute.swift rename to VDS/Components/Label/Attributes/TypographicalStyleLabelAttribute.swift index a1fc2ff2..fe923113 100644 --- a/VDS/Components/Label/Attributes/FontLabelAttribute.swift +++ b/VDS/Components/Label/Attributes/TypographicalStyleLabelAttribute.swift @@ -8,8 +8,9 @@ import Foundation import UIKit -public struct FontLabelAttribute: LabelAttributeModel { - public func isEqual(_ equatable: FontLabelAttribute) -> Bool { +public struct TypographicalStyleLabelAttribute: LabelAttributeModel { + + public func isEqual(_ equatable: TypographicalStyleLabelAttribute) -> Bool { return id == equatable.id && range == equatable.range && color == equatable.color @@ -26,6 +27,7 @@ public struct FontLabelAttribute: LabelAttributeModel { public var color: UIColor public var textPosition: TextPosition public var lineBreakMode: NSLineBreakMode + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -78,5 +80,4 @@ public struct FontLabelAttribute: LabelAttributeModel { attributedString.addAttribute(.paragraphStyle, value: paragraph, range: range) } } - } diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 2b195de8..7fcc931e 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -115,6 +115,7 @@ open class LabelBase: UILabel, Handlerable, ViewProtocol, Resettable { accessibilityCustomActions = [] accessibilityTraits = .staticText numberOfLines = 0 + backgroundColor = .clear } //-------------------------------------------------- diff --git a/VDS/Components/RadioBox/RadioBox.swift b/VDS/Components/RadioBox/RadioBox.swift index d9245790..ee53b936 100644 --- a/VDS/Components/RadioBox/RadioBox.swift +++ b/VDS/Components/RadioBox/RadioBox.swift @@ -180,15 +180,8 @@ open class RadioBoxBase: Control, BinaryColorable, Accessable, DataTrackable{ updateSelector() - selectorView.topAnchor.constraint(equalTo: topAnchor).isActive = true - selectorView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - selectorView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - selectorView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - - mainStackView.topAnchor.constraint(equalTo: selectorView.topAnchor, constant: 16).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: selectorView.leadingAnchor, constant: 16).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: selectorView.trailingAnchor, constant: -16).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: selectorView.bottomAnchor, constant: -16).isActive = true + selectorView.pinToSuperView() + mainStackView.pinToSuperView(.init(top: 16, left: 16, bottom: 16, right: 16)) } func updateLabels() { diff --git a/VDS/Components/RadioBox/RadioBoxGroup.swift b/VDS/Components/RadioBox/RadioBoxGroup.swift index f2c4022f..98cad475 100644 --- a/VDS/Components/RadioBox/RadioBoxGroup.swift +++ b/VDS/Components/RadioBox/RadioBoxGroup.swift @@ -74,10 +74,7 @@ public class RadioBoxGroupBase: SelectorGroupSelected accessibilityTraits = .button addSubview(mainStackView) ensureDevice() - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + mainStackView.pinToSuperView() NotificationCenter.default .publisher(for: UIDevice.orientationDidChangeNotification) diff --git a/VDS/Components/RadioButton/RadioButton.swift b/VDS/Components/RadioButton/RadioButton.swift index f52f2a09..6a1c28c7 100644 --- a/VDS/Components/RadioButton/RadioButton.swift +++ b/VDS/Components/RadioButton/RadioButton.swift @@ -195,10 +195,7 @@ open class RadioButtonBase: Control, Accessable, DataTrackable, BinaryColorable, updateSelector() - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + mainStackView.pinToSuperView() } diff --git a/VDS/Components/RadioButton/RadioButtonGroup.swift b/VDS/Components/RadioButton/RadioButtonGroup.swift index 1b357102..487a8413 100644 --- a/VDS/Components/RadioButton/RadioButtonGroup.swift +++ b/VDS/Components/RadioButton/RadioButtonGroup.swift @@ -79,9 +79,6 @@ public class RadioButtonGroupBase: SelectorGroupSe accessibilityTraits = .button addSubview(mainStackView) - mainStackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + mainStackView.pinToSuperView() } } diff --git a/VDS/Components/RadioSwatch/RadioSwatch.swift b/VDS/Components/RadioSwatch/RadioSwatch.swift index 520262d1..3e987d5a 100644 --- a/VDS/Components/RadioSwatch/RadioSwatch.swift +++ b/VDS/Components/RadioSwatch/RadioSwatch.swift @@ -103,20 +103,17 @@ open class RadioSwatchBase: Control, Accessable, DataTrackable, BinaryColorable selectorView.addSubview(fillView) - selectorView.topAnchor.constraint(equalTo: topAnchor).isActive = true - selectorView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - selectorView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - selectorView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + selectorView.pinToSuperView() let selectorSize = getSelectorSize() - selectorView.heightAnchor.constraint(equalToConstant: selectorSize.height).isActive = true - selectorView.widthAnchor.constraint(equalToConstant: selectorSize.width).isActive = true + selectorView.height(selectorSize.height) + selectorView.width(selectorSize.width) fillView.centerXAnchor.constraint(equalTo: selectorView.centerXAnchor).isActive = true fillView.centerYAnchor.constraint(equalTo: selectorView.centerYAnchor).isActive = true - fillView.heightAnchor.constraint(equalToConstant: fillSize.height).isActive = true - fillView.widthAnchor.constraint(equalToConstant: fillSize.width).isActive = true + fillView.height(fillSize.height) + fillView.width(fillSize.width) } diff --git a/VDS/Components/RadioSwatch/RadioSwatchGroup.swift b/VDS/Components/RadioSwatch/RadioSwatchGroup.swift index 5132ba7a..526df5d5 100644 --- a/VDS/Components/RadioSwatch/RadioSwatchGroup.swift +++ b/VDS/Components/RadioSwatch/RadioSwatchGroup.swift @@ -41,15 +41,13 @@ public class RadioSwatchGroupBase: SelectorGroupSe private let labelHeight: CGFloat = 16.0 private let lineSpacing: CGFloat = 12.0 private let itemSpacing: CGFloat = 16.0 - private var collectionViewHeight: NSLayoutConstraint? - private var collectionViewWidth: NSLayoutConstraint? - fileprivate lazy var collectionView: UICollectionView = { + fileprivate lazy var collectionView: SelfSizingCollectionView = { let layout = UICollectionViewFlowLayout().with { $0.minimumLineSpacing = lineSpacing $0.minimumInteritemSpacing = itemSpacing } - return UICollectionView(frame: .zero, collectionViewLayout: layout).with { + return SelfSizingCollectionView(frame: .zero, collectionViewLayout: layout).with { $0.backgroundColor = .clear $0.showsHorizontalScrollIndicator = false $0.showsVerticalScrollIndicator = false @@ -87,47 +85,29 @@ public class RadioSwatchGroupBase: SelectorGroupSe accessibilityTraits = .button addSubview(label) addSubview(collectionView) - NSLayoutConstraint.activate([ - label.topAnchor.constraint(equalTo: topAnchor), - label.leadingAnchor.constraint(equalTo: leadingAnchor), - label.trailingAnchor.constraint(equalTo: trailingAnchor), - label.heightAnchor.constraint(equalToConstant: labelHeight), - collectionView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: labelSpacing), - collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), - ]) - //TODO: Look at this width stuff, we should NOT need it! - collectionViewWidth = collectionView.widthAnchor.constraint(equalToConstant: cellSize * 20) - collectionViewWidth?.isActive = true - - collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: cellSize) - collectionViewHeight?.isActive = true + + label + .pinTop() + .pinLeading() + .pinTrailing() + .height(labelHeight) + + collectionView + .pinTop(label.bottomAnchor, labelSpacing) + .pinLeading() + .pinTrailing() + .pinBottom() + } open override func layoutSubviews() { super.layoutSubviews() // Accounts for any collection size changes - setHeight() DispatchQueue.main.async { self.collectionView.collectionViewLayout.invalidateLayout() } } - open func setHeight() { - guard selectorViews.count > 0 else { - collectionViewHeight?.constant = 0 - return - } - - // Calculate the height - let swatchesInRow = floor(CGFloat(collectionView.bounds.width/(cellSize + itemSpacing))) - let numberOfRows = ceil(CGFloat(selectorViews.count)/swatchesInRow) - let height = (numberOfRows * cellSize) + (itemSpacing * (numberOfRows-1)) - - collectionViewHeight?.constant = CGFloat(height) - } - public override func initialSetup() { super.initialSetup() collectionView.delegate = self @@ -180,10 +160,7 @@ public class RadioSwatchGroupBase: SelectorGroupSe handler.isUserInteractionEnabled = false cell.subviews.forEach { $0.removeFromSuperview() } cell.addSubview(handler) - handler.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true - handler.leadingAnchor.constraint(equalTo: cell.leadingAnchor).isActive = true - handler.trailingAnchor.constraint(equalTo: cell.trailingAnchor).isActive = true - handler.bottomAnchor.constraint(equalTo: cell.bottomAnchor).isActive = true + handler.pinToSuperView() return cell } diff --git a/VDS/Components/TextFields/EntryField/EntryField.swift b/VDS/Components/TextFields/EntryField/EntryField.swift index 3d8cfce0..150ad842 100644 --- a/VDS/Components/TextFields/EntryField/EntryField.swift +++ b/VDS/Components/TextFields/EntryField/EntryField.swift @@ -178,10 +178,7 @@ open class EntryField: Control, Accessable { stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) - stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + stackView.pinToSuperView() titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() diff --git a/VDS/Components/Toggle/Toggle.swift b/VDS/Components/Toggle/Toggle.swift index 0694d0fb..abf266d2 100644 --- a/VDS/Components/Toggle/Toggle.swift +++ b/VDS/Components/Toggle/Toggle.swift @@ -62,6 +62,11 @@ open class ToggleBase: Control, Accessable, DataTrackable, BinaryColorable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + private var contentView = UIView().with { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.layer.cornerRadius = 2 + } + private var stackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal @@ -252,8 +257,16 @@ open class ToggleBase: Control, Accessable, DataTrackable, BinaryColorable { isAccessibilityElement = true accessibilityTraits = .button - addSubview(stackView) + addSubview(contentView) + contentView.addSubview(stackView) + contentView + .pinTop() + .pinBottom() + .pinLeading() + + contentView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor).isActive = true + //set the h/w to container size, since the width "can" grow if text is there //allow this to be greaterThanEqualTo heightAnchor.constraint(equalToConstant: toggleContainerSize.height).isActive = true @@ -286,10 +299,7 @@ open class ToggleBase: Control, Accessable, DataTrackable, BinaryColorable { knobView.topAnchor.constraint(greaterThanOrEqualTo: toggleView.topAnchor).isActive = true //pin stackview to edges - stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true - stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true - stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true - stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + stackView.pinToSuperView() } public override func reset() { @@ -329,6 +339,5 @@ open class ToggleBase: Control, Accessable, DataTrackable, BinaryColorable { setAccessibilityHint() setAccessibilityValue(isOn) setAccessibilityLabel(isOn) - backgroundColor = surface.color } } diff --git a/VDS/Extensions/UIView.swift b/VDS/Extensions/UIView.swift new file mode 100644 index 00000000..0214c67a --- /dev/null +++ b/VDS/Extensions/UIView.swift @@ -0,0 +1,118 @@ +// +// UIView.swift +// VDS +// +// Created by Matt Bruce on 11/17/22. +// + +import Foundation +import UIKit + +extension UIView { + public func pin(_ view: UIView, with edges: UIEdgeInsets = UIEdgeInsets.zero) { + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: edges.left).isActive = true + trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -edges.right).isActive = true + topAnchor.constraint(equalTo: view.topAnchor, constant: edges.top).isActive = true + bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -edges.bottom).isActive = true + } + + public func pinToSuperView(_ edges: UIEdgeInsets = UIEdgeInsets.zero) { + if let superview { + pin(superview, with: edges) + } + } + + @discardableResult + public func height(_ constant: CGFloat) -> Self { + heightAnchor.constraint(equalToConstant: constant).isActive = true + return self + } + + @discardableResult + public func heightGreaterThanEqual(_ constant: CGFloat) -> Self { + heightAnchor.constraint(greaterThanOrEqualToConstant: constant).isActive = true + return self + } + + @discardableResult + public func heightLessThanEqual(_ constant: CGFloat) -> Self { + heightAnchor.constraint(lessThanOrEqualToConstant: constant).isActive = true + return self + } + + @discardableResult + public func width(_ constant: CGFloat) -> Self { + widthAnchor.constraint(equalToConstant: constant).isActive = true + return self + } + + + @discardableResult + public func widthGreaterThanEqual(_ constant: CGFloat) -> Self { + widthAnchor.constraint(greaterThanOrEqualToConstant: constant).isActive = true + return self + } + + @discardableResult + public func widthLessThanEqual(_ constant: CGFloat) -> Self { + widthAnchor.constraint(lessThanOrEqualToConstant: constant).isActive = true + return self + } + + @discardableResult + public func pinTop(_ constant: CGFloat = 0.0) -> Self { + return pinTop(nil, constant) + } + + @discardableResult + public func pinTop(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { + let found: NSLayoutYAxisAnchor? = anchor ?? superview?.topAnchor + if let found { + topAnchor.constraint(equalTo: found, constant: constant).isActive = true + } + return self + } + + @discardableResult + public func pinBottom(_ constant: CGFloat = 0.0) -> Self { + return pinBottom(nil, constant) + } + + @discardableResult + public func pinBottom(_ anchor: NSLayoutYAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { + let found: NSLayoutYAxisAnchor? = anchor ?? superview?.bottomAnchor + if let found { + bottomAnchor.constraint(equalTo: found, constant: -constant).isActive = true + } + return self + } + + @discardableResult + public func pinLeading(_ constant: CGFloat = 0.0) -> Self { + return pinLeading(nil, constant) + } + + @discardableResult + public func pinLeading(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { + let found: NSLayoutXAxisAnchor? = anchor ?? superview?.leadingAnchor + if let found { + leadingAnchor.constraint(equalTo: found, constant: constant).isActive = true + } + return self + } + + @discardableResult + public func pinTrailing(_ constant: CGFloat = 0.0) -> Self { + return pinTrailing(nil, constant) + } + + @discardableResult + public func pinTrailing(_ anchor: NSLayoutXAxisAnchor? = nil, _ constant: CGFloat = 0.0) -> Self { + let found: NSLayoutXAxisAnchor? = anchor ?? superview?.trailingAnchor + if let found { + trailingAnchor.constraint(equalTo: found, constant: -constant).isActive = true + } + return self + } + +} diff --git a/VDS/Protocols/Disabling.swift b/VDS/Protocols/Disabling.swift index c0be6d22..e800e174 100644 --- a/VDS/Protocols/Disabling.swift +++ b/VDS/Protocols/Disabling.swift @@ -7,6 +7,6 @@ import Foundation -public protocol Disabling { +public protocol Disabling: AnyObject { var disabled: Bool { get set } } diff --git a/VDS/Protocols/Surfaceable.swift b/VDS/Protocols/Surfaceable.swift index d5283a37..a26ddda4 100644 --- a/VDS/Protocols/Surfaceable.swift +++ b/VDS/Protocols/Surfaceable.swift @@ -16,6 +16,6 @@ public enum Surface: String, Codable, Equatable { } } -public protocol Surfaceable { +public protocol Surfaceable: AnyObject { var surface: Surface { get set } }