diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index c86f3116..17cfb42f 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -20,8 +20,10 @@ 5F21D7BF28DCEB3D003E7CD6 /* Useable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */; }; 5FC35BE328D51405004EBEAC /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FC35BE228D51405004EBEAC /* Button.swift */; }; 7115BD3C2B84C0C200E0A610 /* TileContainerChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */; }; - 71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */; }; + 71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */; }; 71C02B382B7BD98F00E93E66 /* NotificationChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */; }; + 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */; }; + 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */; }; EA0B18022A9E236900F2D0CD /* SelectorGroupBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */; }; EA0B18052A9E2D2D00F2D0CD /* SelectorBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */; }; EA0B18062A9E2D2D00F2D0CD /* SelectorItemBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */; }; @@ -192,8 +194,10 @@ 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Useable.swift; sourceTree = ""; }; 5FC35BE228D51405004EBEAC /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; 7115BD3B2B84C0C200E0A610 /* TileContainerChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = TileContainerChangeLog.txt; sourceTree = ""; }; - 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dropshadowable.swift; sourceTree = ""; }; + 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowable.swift; sourceTree = ""; }; 71C02B372B7BD98F00E93E66 /* NotificationChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = NotificationChangeLog.txt; sourceTree = ""; }; + 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceConfigurationValue.swift; sourceTree = ""; }; + 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowConfiguration.swift; sourceTree = ""; }; EA0B18012A9E236900F2D0CD /* SelectorGroupBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorGroupBase.swift; sourceTree = ""; }; EA0B18032A9E2D2D00F2D0CD /* SelectorBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorBase.swift; sourceTree = ""; }; EA0B18042A9E2D2D00F2D0CD /* SelectorItemBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectorItemBase.swift; sourceTree = ""; }; @@ -584,7 +588,7 @@ EA3361B7288B2AAA0071C351 /* ViewProtocol.swift */, EAB1D2CC28ABE76000DAE764 /* Withable.swift */, 5F21D7BE28DCEB3D003E7CD6 /* Useable.swift */, - 71BFA7092B7F70E6000DCE33 /* Dropshadowable.swift */, + 71BFA7092B7F70E6000DCE33 /* DropShadowable.swift */, ); path = Protocols; sourceTree = ""; @@ -603,6 +607,8 @@ isa = PBXGroup; children = ( EA3361BC288B2C760071C351 /* TypeAlias.swift */, + 71FC86DD2B9738B900700965 /* SurfaceConfigurationValue.swift */, + 71FC86DF2B973AE500700965 /* DropShadowConfiguration.swift */, ); path = Utilities; sourceTree = ""; @@ -1030,7 +1036,7 @@ EAB2376A29E9E59100AABE9A /* TooltipLaunchable.swift in Sources */, 18A65A042B96F050006602CC /* BreadcrumbItem.swift in Sources */, EAB2375D29E8789100AABE9A /* Tooltip.swift in Sources */, - 71BFA70A2B7F70E6000DCE33 /* Dropshadowable.swift in Sources */, + 71BFA70A2B7F70E6000DCE33 /* DropShadowable.swift in Sources */, EA0D1C452A6AD73000E5C127 /* RawRepresentable.swift in Sources */, EA985C23296E033A00F2FF2E /* TextArea.swift in Sources */, EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */, @@ -1055,6 +1061,7 @@ EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */, EAF7F0952899861000B287F5 /* CheckboxItem.swift in Sources */, EA985BE82968951C00F2FF2E /* TileletTitleModel.swift in Sources */, + 71FC86DE2B9738B900700965 /* SurfaceConfigurationValue.swift in Sources */, EA297A5529FB07760031ED56 /* TooltipLabelAttribute.swift in Sources */, EA985BEA29689B6D00F2FF2E /* TileletSubTitleModel.swift in Sources */, EA3361C9289054C50071C351 /* Surfaceable.swift in Sources */, @@ -1125,6 +1132,7 @@ EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, + 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */, EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */, EA3362302891EB4A0071C351 /* Font.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 9ca86d8c..869c40e0 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -102,7 +102,6 @@ open class SelectorItemBase: Control, Errorable, /// Instead of use labelText and labelTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes. open var labelAttributedText: NSAttributedString? { didSet { - label.useAttributedText = !(labelAttributedText?.string.isEmpty ?? true) label.attributedText = labelAttributedText setNeedsUpdate() } @@ -117,7 +116,6 @@ open class SelectorItemBase: Control, Errorable, /// Instead of use childText and childTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes. open var childAttributedText: NSAttributedString? { didSet { - childLabel.useAttributedText = !(childAttributedText?.string.isEmpty ?? true) childLabel.attributedText = childAttributedText setNeedsUpdate() } diff --git a/VDS/Classes/SelfSizingCollectionView.swift b/VDS/Classes/SelfSizingCollectionView.swift index c50082c4..ad5d5661 100644 --- a/VDS/Classes/SelfSizingCollectionView.swift +++ b/VDS/Classes/SelfSizingCollectionView.swift @@ -34,10 +34,17 @@ public final class SelfSizingCollectionView: UICollectionView { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - private var contentSizeObservation: NSKeyValueObservation? private var collectionViewHeight: NSLayoutConstraint? private var anyCancellable: AnyCancellable? - + private var contentSizeSubject = CurrentValueSubject(.zero) + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var contentSizePublisher: AnyPublisher { + contentSizeSubject.eraseToAnyPublisher() + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -45,7 +52,6 @@ public final class SelfSizingCollectionView: UICollectionView { /// The natural size for the receiving view, considering only properties of the view itself. public override var intrinsicContentSize: CGSize { let contentSize = self.contentSize - //print(#function, contentSize) return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) } @@ -67,18 +73,17 @@ public final class SelfSizingCollectionView: UICollectionView { //ensure autoLayout uses intrinsic height setContentHuggingPriority(.required, for: .vertical) setContentCompressionResistancePriority(.required, for: .vertical) - collectionViewHeight = heightAnchor.constraint(equalToConstant: 0).activate() - - // 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() - if let height = change.newValue?.height { - self?.collectionViewHeight?.constant = height + collectionViewHeight = height(constant: 0, priority: .defaultHigh) + + anyCancellable = self.publisher(for: \.contentSize, options: [.new]) + .sink { [weak self] compare in + guard let self else { return } + if compare.height != self.collectionViewHeight?.constant { + self.invalidateIntrinsicContentSize() + self.collectionViewHeight?.constant = compare.height + self.contentSizeSubject.send(compare) } } - } } } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 255c6f64..cc096d41 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -102,6 +102,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { + initialSetupPerformed = true backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift index 7d506961..a51371c2 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift @@ -9,6 +9,7 @@ import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens +import Combine /// A button group contains combinations of related CTAs including ``Button``, ``TextLink``, and ``TextLinkCaret``. This group component controls a combination's orientation, spacing, size and allowable size pairings. @objc(VDSButtonGroup) @@ -98,6 +99,8 @@ open class ButtonGroup: View { buttons.forEach { $0.surface = surface } } } + + open var contentSizePublisher: AnyPublisher { collectionView.contentSizePublisher } //-------------------------------------------------- // MARK: - Private Properties @@ -108,6 +111,7 @@ open class ButtonGroup: View { $0.delegate = self } + /// CollectionView that renders the array of buttonBase obects. fileprivate lazy var collectionView: SelfSizingCollectionView = { return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with { diff --git a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift index 16f0416f..65968214 100644 --- a/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift +++ b/VDS/Components/Icon/ButtonIcon/ButtonIcon.swift @@ -232,19 +232,26 @@ open class ButtonIcon: Control, Changeable, FormFieldable { }() } - private struct LowContrastColorFillFloatingConfiguration: Configuration, Dropshadowable { + private struct LowContrastColorFillFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .colorFill var floating: Bool = true var backgroundColorConfiguration: AnyColorable = { SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable() }() - var shadowColorConfiguration: AnyColorable = { - SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() - }() - var shadowOpacity: CGFloat = 0.16 - var shadowOffset: CGSize = .init(width: 0, height: 2) - var shadowRadius: CGFloat = 4 + private let dropshadow1Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.12), CGFloat(0.22)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(10), CGFloat(12)) + } + private let dropshadow2Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.05), CGFloat(0.15)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(4), CGFloat(6)) + } + var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } private struct LowContrastMediaConfiguration: Configuration, Borderable { @@ -260,19 +267,26 @@ open class ButtonIcon: Control, Changeable, FormFieldable { }() } - private struct LowContrastMediaFloatingConfiguration: Configuration, Dropshadowable { + private struct LowContrastMediaFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .lowContrast var surfaceType: SurfaceType = .media var floating: Bool = true var backgroundColorConfiguration: AnyColorable = { SurfaceColorConfiguration(VDSColor.paletteWhite, VDSColor.paletteGray20).eraseToAnyColorable() }() - var shadowColorConfiguration: AnyColorable = { - SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() - }() - var shadowOpacity: CGFloat = 0.16 - var shadowOffset: CGSize = .init(width: 0, height: 2) - var shadowRadius: CGFloat = 4 + private let dropshadow1Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.12), CGFloat(0.22)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(10), CGFloat(12)) + } + private let dropshadow2Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.05), CGFloat(0.15)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(4), CGFloat(6)) + } + var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } private struct HighContrastConfiguration: Configuration { @@ -291,7 +305,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable { }() } - private struct HighContrastFloatingConfiguration: Configuration, Dropshadowable { + private struct HighContrastFloatingConfiguration: Configuration, DropShadowableConfiguration { var kind: Kind = .highContrast var surfaceType: SurfaceType = .colorFill var floating: Bool = true @@ -305,12 +319,19 @@ open class ButtonIcon: Control, Changeable, FormFieldable { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forState: [.selected, .disabled]) }.eraseToAnyColorable() }() - var shadowColorConfiguration: AnyColorable = { - SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() - }() - var shadowOpacity: CGFloat = 0.16 - var shadowOffset: CGSize = .init(width: 0, height: 2) - var shadowRadius: CGFloat = 6 + private let dropshadow1Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.22), CGFloat(0.12)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 1), .init(width: 0, height: 1)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(12), CGFloat(10)) + } + private let dropshadow2Configuration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration(VDSColor.paletteBlack, VDSColor.paletteBlack).eraseToAnyColorable() + $0.shadowOpacityConfiguration = SurfaceConfigurationValue(CGFloat(0.15), CGFloat(0.05)) + $0.shadowOffsetConfiguration = SurfaceConfigurationValue(.init(width: 0, height: 2), .init(width: 0, height: 2)) + $0.shadowRadiusConfiguration = SurfaceConfigurationValue(CGFloat(6), CGFloat(4)) + } + var configurations: [DropShadowable] { [dropshadow1Configuration, dropshadow2Configuration] } } private var badgeIndicatorDefaultSize: CGSize = .zero @@ -322,7 +343,7 @@ open class ButtonIcon: Control, Changeable, FormFieldable { open override func setup() { super.setup() isAccessibilityElement = true - accessibilityTraits = .image + accessibilityTraits = .button accessibilityElements = [badgeIndicator] //create a layoutGuide for the icon to key off of @@ -452,12 +473,6 @@ open class ButtonIcon: Control, Changeable, FormFieldable { layer.borderColor = nil layer.borderWidth = 0 } - - if let dropshadowable = currentConfig as? Dropshadowable { - addDropShadow(dropshadowable) - } else { - removeDropShadows() - } badgeIndicatorCenterXConstraint?.constant = badgeIndicatorOffset.x + badgeIndicatorDefaultSize.width/2 badgeIndicatorCenterYConstraint?.constant = badgeIndicatorOffset.y + badgeIndicatorDefaultSize.height/2 @@ -467,6 +482,12 @@ open class ButtonIcon: Control, Changeable, FormFieldable { if showBadgeIndicator { updateExpandDirectionalConstraints() } + + if let configurations = (currentConfig as? DropShadowableConfiguration)?.configurations { + addDropShadows(configurations) + } else { + removeDropShadows() + } } //-------------------------------------------------- diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 67e53abe..60e88740 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -42,6 +42,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + private enum TextSetMode { + case text + case attributedText + } + + private var textSetMode: TextSetMode = .text + private var initialSetupPerformed = false private var edgeInsets: UIEdgeInsets { textStyle.edgeInsets } @@ -102,10 +109,6 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() open var shouldUpdateView: Bool = true - - /// Determines if the label should use its own attributedText property instead of rendering the attributedText propert - /// based of other local properties, such as textStyle, textColor, surface, etc... The default value is false. - open var useAttributedText: Bool = false /// Will determine if a scaled font should be used for the font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }} @@ -128,19 +131,40 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Line break mode for the label, default is set to word wrapping. open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }} - - private var _text: String? - + /// Text that will be used in the label. - override open var text: String? { - get { _text } + override open var text: String! { + get { super.text } set { - if _text != newValue || newValue != attributedText?.string { - _text = newValue - useAttributedText = false - attributes?.removeAll() - setNeedsUpdate() + textSetMode = .text + styleText(newValue) + } + } + + ///AttributedText that will be used in the label. + override open var attributedText: NSAttributedString? { + get { super.attributedText } + set { + textSetMode = .attributedText + styleAttributedText(newValue) + } + } + + override open var font: UIFont! { + didSet { + if let font, initialSetupPerformed { + textStyle = TextStyle.convert(font: font) } + setNeedsUpdate() + } + } + + override open var textColor: UIColor! { + didSet { + if let textColor, initialSetupPerformed { + textColorConfiguration = SurfaceColorConfiguration(textColor, textColor).eraseToAnyColorable() + } + setNeedsUpdate() } } @@ -162,13 +186,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { + initialSetupPerformed = true //register for ContentSizeChanges NotificationCenter .Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification) .sink { [weak self] notification in self?.setNeedsUpdate() }.store(in: &subscribers) - backgroundColor = .clear numberOfLines = 0 lineBreakMode = .byWordWrapping @@ -200,30 +224,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open func updateView() { - if !useAttributedText { - if let text { - accessibilityCustomActions = [] - - //create the primary string - let mutableText = NSMutableAttributedString.mutableText(for: text, - textStyle: textStyle, - useScaledFont: useScaledFont, - textColor: textColorConfiguration.getColor(self), - alignment: textAlignment, - lineBreakMode: lineBreakMode) - - applyAttributes(mutableText) - - //set the attributed text - attributedText = mutableText - - //force a drawText - setNeedsDisplay() - - setNeedsLayout() - layoutIfNeeded() - } - } + restyleText() + + //force a drawText + setNeedsDisplay() + + setNeedsLayout() + layoutIfNeeded() } open func updateAccessibility() { @@ -269,6 +276,56 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- + private func restyleText() { + if textSetMode == .text { + styleText(text) + } else { + styleAttributedText(attributedText) + } + } + + private func styleText(_ newValue: String!) { + defer { invalidateIntrinsicContentSize() } + guard let newValue else { + // We don't need to use attributed text + super.attributedText = nil + super.text = newValue + return + } + + accessibilityCustomActions = [] + + //create the primary string + let mutableText = NSMutableAttributedString.mutableText(for: newValue, + textStyle: textStyle, + useScaledFont: useScaledFont, + textColor: textColorConfiguration.getColor(self), + alignment: textAlignment, + lineBreakMode: lineBreakMode) + + applyAttributes(mutableText) + + // Set attributed text to match typography + super.attributedText = mutableText + + } + + private func styleAttributedText(_ newValue: NSAttributedString?) { + defer { invalidateIntrinsicContentSize() } + guard let newValue = newValue else { + // We don't need any additional styling + super.attributedText = newValue + return + } + + let mutableText = NSMutableAttributedString(attributedString: newValue) + + applyAttributes(mutableText) + + // Modify attributed text to match typography + super.attributedText = mutableText + } + private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) { actions = [] diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 81f3f709..cfeb54e6 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -100,7 +100,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable { /// If provided, the RadioBox textAttributedText will be rendered. open var textAttributedText: NSAttributedString? { didSet { - textLabel.useAttributedText = !(textAttributedText?.string.isEmpty ?? true) textLabel.attributedText = textAttributedText setNeedsUpdate() } @@ -115,7 +114,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable { /// If provided, the RadioBox subTextAttributedText will be rendered. open var subTextAttributedText: NSAttributedString? { didSet { - subTextLabel.useAttributedText = !(subTextAttributedText?.string.isEmpty ?? true) subTextLabel.attributedText = subTextAttributedText setNeedsUpdate() } @@ -130,7 +128,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable { /// If provided, the RadioBox subTextRightAttributedText will be rendered. open var subTextRightAttributedText: NSAttributedString? { didSet { - subTextRightLabel.useAttributedText = !(subTextRightAttributedText?.string.isEmpty ?? true) subTextRightLabel.attributedText = subTextRightAttributedText setNeedsUpdate() } diff --git a/VDS/Components/TextFields/EntryFieldBase.swift b/VDS/Components/TextFields/EntryFieldBase.swift index 6964ae0a..ae91cc7a 100644 --- a/VDS/Components/TextFields/EntryFieldBase.swift +++ b/VDS/Components/TextFields/EntryFieldBase.swift @@ -154,13 +154,13 @@ open class EntryFieldBase: Control, Changeable, FormFieldable { open var showError: Bool = false { didSet { setNeedsUpdate() } } /// Whether or not to show the internal error - internal var showInternalError: Bool = false { didSet { setNeedsUpdate() } } + open internal(set) var hasInternalError: Bool = false { didSet { setNeedsUpdate() } } /// Override UIControl state to add the .error state if showError is true. open override var state: UIControl.State { get { var state = super.state - if showError || showInternalError { + if showError || hasInternalError { state.insert(.error) } return state @@ -380,7 +380,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldable { } open func updateErrorLabel(){ - if showError, showInternalError, let errorText, let internalErrorText { + if showError, hasInternalError, let errorText, let internalErrorText { errorLabel.text = [internalErrorText, errorText].joined(separator: "\n") errorLabel.surface = surface errorLabel.isEnabled = isEnabled @@ -398,7 +398,7 @@ open class EntryFieldBase: Control, Changeable, FormFieldable { icon.color = VDSColor.paletteBlack icon.surface = surface icon.isHidden = !isEnabled - } else if showInternalError, let internalErrorText { + } else if hasInternalError, let internalErrorText { errorLabel.text = internalErrorText errorLabel.surface = surface errorLabel.isEnabled = isEnabled diff --git a/VDS/Components/TextFields/TextArea/TextArea.swift b/VDS/Components/TextFields/TextArea/TextArea.swift index 317748e4..d30c187f 100644 --- a/VDS/Components/TextFields/TextArea/TextArea.swift +++ b/VDS/Components/TextFields/TextArea/TextArea.swift @@ -247,16 +247,16 @@ open class TextArea: EntryFieldBase { let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)" if let maxLength, maxLength > 0 { if count > maxLength { - showInternalError = true + hasInternalError = true internalErrorText = "You have exceeded the character limit." return countStr } else { - showInternalError = false + hasInternalError = false internalErrorText = nil return ("\(countStr)" + "/" + "\(maxLength)") } } else { - showInternalError = false + hasInternalError = false internalErrorText = nil return nil } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index ea96ed6e..ffb355d1 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -94,6 +94,7 @@ open class TextView: UITextView, ViewProtocol { //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { + initialSetupPerformed = true backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] diff --git a/VDS/Components/TileContainer/TileContainer.swift b/VDS/Components/TileContainer/TileContainer.swift index 98090ae4..8dc2d9dd 100644 --- a/VDS/Components/TileContainer/TileContainer.swift +++ b/VDS/Components/TileContainer/TileContainer.swift @@ -184,9 +184,15 @@ open class TileContainer: Control { // MARK: - Configuration //-------------------------------------------------- private let cornerRadius = VDSFormControls.borderradius * 2 - private var backgroundColorConfiguration = BackgroundColorConfiguration() - private var dropshadowConfiguration = DropshadowConfiguration() + private let dropShadowConfiguration = DropShadowConfiguration().with { + $0.shadowColorConfiguration = SurfaceColorConfiguration().with { + $0.lightColor = VDSColor.elementsPrimaryOnlight + }.eraseToAnyColorable() + $0.shadowOffsetConfiguration = .init(.init(width: 0, height: 6), .zero) + $0.shadowRadiusConfiguration = .init(3.0, 0.0) + $0.shadowOpacityConfiguration = .init(0.01, 0.0) + } private var borderColorConfiguration = SurfaceColorConfiguration().with { $0.lightColor = VDSColor.elementsLowcontrastOnlight @@ -311,7 +317,7 @@ open class TileContainer: Control { heightConstraint?.isActive = false } if showDropShadows, surface == .light { - addDropShadow(dropshadowConfiguration) + addDropShadow(dropShadowConfiguration) } else { removeDropShadows() } @@ -397,15 +403,6 @@ open class TileContainer: Control { extension TileContainer { - struct DropshadowConfiguration: Dropshadowable { - var shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().with { - $0.lightColor = VDSColor.elementsPrimaryOnlight - }.eraseToAnyColorable() - var shadowOpacity: CGFloat = 0.01 - var shadowOffset: CGSize = .init(width: 0, height: 6) - var shadowRadius: CGFloat = 3 - } - final class BackgroundColorConfiguration: ObjectColorable { typealias ObjectType = TileContainer diff --git a/VDS/Components/TitleLockup/TitleLockup.swift b/VDS/Components/TitleLockup/TitleLockup.swift index e11b674d..3802b529 100644 --- a/VDS/Components/TitleLockup/TitleLockup.swift +++ b/VDS/Components/TitleLockup/TitleLockup.swift @@ -68,6 +68,7 @@ open class TitleLockup: View { /// Label used to render the eyebrow model. open var eyebrowLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .vertical) } /// Model used in rendering the eyebrow label. @@ -77,6 +78,7 @@ open class TitleLockup: View { /// Label used to render the title model. open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .vertical) $0.accessibilityTraits.insert([.header]) } @@ -87,6 +89,7 @@ open class TitleLockup: View { /// Label used to render the subtitle model. open var subTitleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .vertical) } /// Model used in rendering the subtitle label. @@ -380,7 +383,7 @@ open class TitleLockup: View { } //pin the last view to the bottom of this view - previousView?.pinBottom(0, .defaultHigh) + previousView?.pinBottom(0) //debugging for borders eyebrowLabel.debugBorder(show: hasDebugBorder, color: .green) diff --git a/VDS/Fonts/Font.swift b/VDS/Fonts/Font.swift index c5caaa32..f4ba798d 100644 --- a/VDS/Fonts/Font.swift +++ b/VDS/Fonts/Font.swift @@ -6,15 +6,17 @@ // import Foundation +import UIKit /// Enum that is matched up for the Verizon fonts. -public enum Font: String, FontProtocol { +public enum Font: FontProtocol { case edsBold case edsRegular case dsLight case etxBold case etxRegular - + case custom(UIFont) + public var fontName: String { switch self { case .edsBold: @@ -27,11 +29,32 @@ public enum Font: String, FontProtocol { return "VerizonNHGeTX-Bold" case .etxRegular: return "VerizonNHGeTX-Regular" + case .custom(let font): + return font.fontName } } + public static var allCases: [Font] { + [.edsBold, .edsRegular, .dsLight, .etxBold, .etxRegular] + } + /// File Extension for each of the Font enums. public var fontFileExtension: String { return "otf" } + + /// Returns a UIFont for the fontName and size given. + /// - Parameters: + /// - size: Size of the font + /// - Returns: UIFont for the fontName and Size. + public func font(ofSize size: CGFloat) -> UIFont{ + DispatchQueue.once(block: { self.register() }) + switch self { + case .custom(let font): + return font + default: + guard let found = UIFont(name: self.fontName, size: size) else { return .systemFont(ofSize: size) } + return found + } + } } diff --git a/VDS/Fonts/FontProtocol.swift b/VDS/Fonts/FontProtocol.swift index 9ee21939..df6ac6a3 100644 --- a/VDS/Fonts/FontProtocol.swift +++ b/VDS/Fonts/FontProtocol.swift @@ -9,9 +9,10 @@ import Foundation import UIKit /// Used in Classes that require Fonts -public protocol FontProtocol: CaseIterable, RawRepresentable, Hashable { +public protocol FontProtocol { var fontFileExtension: String { get } var fontName: String { get } + static var allCases: [Self] { get } } extension FontProtocol { diff --git a/VDS/Protocols/DropShadowable.swift b/VDS/Protocols/DropShadowable.swift new file mode 100644 index 00000000..b569ecab --- /dev/null +++ b/VDS/Protocols/DropShadowable.swift @@ -0,0 +1,89 @@ +// +// DropShadowable.swift +// VDS +// +// Created by Bandaru, Krishna Kishore on 16/02/24. +// + +import Foundation +import UIKit + +/** + DropShadowable protocol helps with the configuration values for adding drop shadows for light & dark surfaces. +*/ +protocol DropShadowable { + ///Shadow Color configuration for light and dark surfaces + var shadowColorConfiguration: AnyColorable { get set } + ///Shadow Opacity configuration for light and dark surfaces + var shadowOpacityConfiguration: SurfaceConfigurationValue { get set } + ///Shadow Offset configuration for light and dark surfaces + var shadowOffsetConfiguration: SurfaceConfigurationValue { get set } + ///Shadow Radius configuration for light and dark surfaces + var shadowRadiusConfiguration: SurfaceConfigurationValue { get set } +} + +/** + DropShadowableConfiguration protocol helps with multiple drop shadows configurations can be added to a view. + */ +protocol DropShadowableConfiguration { + + ///Configurations are the DropShadowable list, these are applied on the view + var configurations: [DropShadowable] { get } +} + +/** + Extension on ViewProtocol for adding drop shadows & gradient layer on view. + */ +extension ViewProtocol where Self: UIView { + + func addDropShadow(_ config: DropShadowable) { + addDropShadows([config]) + } + + func addDropShadows(_ configs: [DropShadowable]) { + removeDropShadows() + layer.backgroundColor = backgroundColor?.cgColor + layer.masksToBounds = false + for config in configs { + let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius) + let shadowLayer = CALayer() + shadowLayer.shadowPath = shadowPath.cgPath + shadowLayer.frame = bounds + shadowLayer.position = .init(x: bounds.midX, y: bounds.midY) + shadowLayer.backgroundColor = backgroundColor?.cgColor + shadowLayer.cornerRadius = layer.cornerRadius + shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor + shadowLayer.shadowOpacity = Float(config.shadowOpacityConfiguration.value(for: self)) + shadowLayer.shadowOffset = config.shadowOffsetConfiguration.value(for: self) + shadowLayer.shadowRadius = config.shadowRadiusConfiguration.value(for: self) + shadowLayer.name = "dropShadowLayer" + shadowLayer.shouldRasterize = true + shadowLayer.rasterizationScale = UIScreen.main.scale + layer.insertSublayer(shadowLayer, at: 0) + } + } + + func removeDropShadows() { + layer.sublayers?.removeAll { $0.name == "dropShadowLayer" } + } + + func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) { + removeGradientLayer() + let gradientLayer = CAGradientLayer() + gradientLayer.frame = bounds + gradientLayer.startPoint = CGPoint(x: 0, y: 1) + gradientLayer.endPoint = CGPoint(x: 1, y: 0) + gradientLayer.position = center + gradientLayer.shouldRasterize = true + gradientLayer.backgroundColor = UIColor.clear.cgColor + gradientLayer.rasterizationScale = UIScreen.main.scale + gradientLayer.cornerRadius = layer.cornerRadius + gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor] + gradientLayer.name = "gradientLayer" + layer.insertSublayer(gradientLayer, at: 0) + } + + func removeGradientLayer() { + layer.sublayers?.removeAll { $0.name == "gradientLayer" } + } +} diff --git a/VDS/Protocols/Dropshadowable.swift b/VDS/Protocols/Dropshadowable.swift deleted file mode 100644 index 77f1a475..00000000 --- a/VDS/Protocols/Dropshadowable.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Dropshadowable.swift -// VDS -// -// Created by Bandaru, Krishna Kishore on 16/02/24. -// - -import Foundation -import UIKit - -protocol Dropshadowable { - - var shadowColorConfiguration: AnyColorable { get set } - var shadowOpacity: CGFloat { get set } - var shadowOffset: CGSize { get set } - var shadowRadius: CGFloat { get set } -} - -extension ViewProtocol where Self: UIView { - - func addDropShadow(_ config: Dropshadowable) { - removeDropShadows() - layer.backgroundColor = backgroundColor?.cgColor - layer.masksToBounds = false - let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius) - let shadowLayer = CALayer() - shadowLayer.shadowPath = shadowPath.cgPath - shadowLayer.frame = bounds - shadowLayer.position = center - shadowLayer.backgroundColor = UIColor.clear.cgColor - shadowLayer.cornerRadius = layer.cornerRadius - shadowLayer.shadowColor = config.shadowColorConfiguration.getColor(self).cgColor - shadowLayer.shadowOpacity = Float(config.shadowOpacity) - shadowLayer.shadowOffset = .init(width: config.shadowOffset.width, height: config.shadowOffset.height) - shadowLayer.shadowRadius = config.shadowRadius - shadowLayer.name = "dropShadowLayer" - shadowLayer.shouldRasterize = true - shadowLayer.rasterizationScale = UIScreen.main.scale - layer.insertSublayer(shadowLayer, at: 0) - } - - func removeDropShadows() { - layer.sublayers?.removeAll { $0.name == "dropShadowLayer" } - } - - func addGradientLayer(with firstColor: UIColor, secondColor: UIColor) { - removeGradientLayer() - let gradientLayer = CAGradientLayer() - gradientLayer.frame = bounds - gradientLayer.startPoint = CGPoint(x: 0, y: 1) - gradientLayer.endPoint = CGPoint(x: 1, y: 0) - gradientLayer.position = center - gradientLayer.shouldRasterize = true - gradientLayer.rasterizationScale = UIScreen.main.scale - gradientLayer.cornerRadius = layer.cornerRadius - gradientLayer.colors = [firstColor.cgColor, secondColor.cgColor] - gradientLayer.name = "gradientLayer" - layer.insertSublayer(gradientLayer, at: 0) - } - - func removeGradientLayer() { - layer.sublayers?.removeAll { $0.name == "gradientLayer" } - } -} diff --git a/VDS/Typography/Typogprahy+Styles.swift b/VDS/Typography/Typogprahy+Styles.swift index ec144617..0136d3c4 100644 --- a/VDS/Typography/Typogprahy+Styles.swift +++ b/VDS/Typography/Typogprahy+Styles.swift @@ -209,6 +209,13 @@ extension TextStyle { boldMicro ] } + + public static func convert(font: UIFont) -> TextStyle { + guard let found = allCases.first(where: { font.fontName == $0.fontFace.fontName && font.pointSize == $0.pointSize} ) else { + return TextStyle(rawValue: "Custom\(font.fontName)", fontFace: .custom(font), pointSize: font.pointSize) + } + return found + } } extension TextStyle { diff --git a/VDS/Utilities/DropShadowConfiguration.swift b/VDS/Utilities/DropShadowConfiguration.swift new file mode 100644 index 00000000..e0cc3dd8 --- /dev/null +++ b/VDS/Utilities/DropShadowConfiguration.swift @@ -0,0 +1,33 @@ +// +// DropShadowConfiguration.swift +// VDS +// +// Created by Bandaru, Krishna Kishore on 05/03/24. +// + +import Foundation + +/** + DropShadowConfiguration confirms to DropShadowable where it has configurable properties required for drop shadow +*/ +final class DropShadowConfiguration: DropShadowable, ObjectWithable { + + typealias CGFloatConfigurationValue = SurfaceConfigurationValue + typealias CGSizeConfigurationValue = SurfaceConfigurationValue + + ///Shadow Color configuration for light and dark surfaces + var shadowColorConfiguration: AnyColorable + ///Shadow Opacity configuration for light and dark surfaces + var shadowOpacityConfiguration: CGFloatConfigurationValue + ///Shadow Offset configuration for light and dark surfaces + var shadowOffsetConfiguration: CGSizeConfigurationValue + ///Shadow Radius configuration for light and dark surfaces + var shadowRadiusConfiguration: CGFloatConfigurationValue + + init(shadowColorConfiguration: AnyColorable = SurfaceColorConfiguration().eraseToAnyColorable(), shadowOpacity: CGFloatConfigurationValue = CGFloatConfigurationValue(1.0, 1.0), shadowOffset: CGSizeConfigurationValue = CGSizeConfigurationValue(.zero, .zero), shadowRadius: CGFloatConfigurationValue = CGFloatConfigurationValue(1.0, 1.0)) { + self.shadowColorConfiguration = shadowColorConfiguration + self.shadowOpacityConfiguration = shadowOpacity + self.shadowOffsetConfiguration = shadowOffset + self.shadowRadiusConfiguration = shadowRadius + } +} diff --git a/VDS/Utilities/SurfaceConfigurationValue.swift b/VDS/Utilities/SurfaceConfigurationValue.swift new file mode 100644 index 00000000..1b304d2a --- /dev/null +++ b/VDS/Utilities/SurfaceConfigurationValue.swift @@ -0,0 +1,31 @@ +// +// SurfaceConfigurationValue.swift +// VDS +// +// Created by Bandaru, Krishna Kishore on 05/03/24. +// + +import Foundation + +/** +SurfaceConfiguration is a type that holds the generic datatype for light surface & dark surface and returns the value based on the surface. +*/ +struct SurfaceConfigurationValue { + + var lightValue: ValueType + var darkValue: ValueType + + public init(_ lightValue: ValueType, _ darkValue: ValueType) { + self.lightValue = lightValue + self.darkValue = darkValue + } + + public init(value: ValueType) { + self.lightValue = value + self.darkValue = value + } + + public func value(for object: Surfaceable) -> ValueType { + object.surface == .light ? lightValue : darkValue + } +}