Merge branch 'feature/textLink' into 'develop'

don't default to .black

See merge request BPHV_MIPS/vds_ios!5
This commit is contained in:
Bruce, Matt R 2022-11-02 21:51:40 +00:00
commit 47a3710cf4
5 changed files with 468 additions and 11 deletions

View File

@ -56,6 +56,8 @@
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 */; };
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 */; };
EAC9258C2911C9DE00091998 /* TextEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC925872911C9DE00091998 /* TextEntryField.swift */; };
EAC9258F2911C9DE00091998 /* EntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC9258B2911C9DE00091998 /* EntryField.swift */; };
@ -141,6 +143,8 @@
EAB1D2CE28ABEF2B00DAE764 /* Typography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typography.swift; sourceTree = "<group>"; };
EAB1D2E328AE842000DAE764 /* Publisher+Bind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Publisher+Bind.swift"; sourceTree = "<group>"; };
EAB1D2E928AE84AA00DAE764 /* UIControlPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControlPublisher.swift; sourceTree = "<group>"; };
EAC9257C29119B5400091998 /* TextLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLink.swift; sourceTree = "<group>"; };
EAC925822911B35300091998 /* TextLinkCaret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLinkCaret.swift; sourceTree = "<group>"; };
EAC925872911C9DE00091998 /* TextEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEntryField.swift; sourceTree = "<group>"; };
EAC9258B2911C9DE00091998 /* EntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = "<group>"; };
EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = "<group>"; };
@ -193,6 +197,16 @@
path = Button;
sourceTree = "<group>";
};
EA0FC2BE2912D18200DF80B4 /* Buttons */ = {
isa = PBXGroup;
children = (
5FC35BE128D513EB004EBEAC /* Button */,
EAC9257E29119B5D00091998 /* TextLink */,
EAC925812911B34300091998 /* TextLinkCaret */,
);
path = Buttons;
sourceTree = "<group>";
};
EA1F265F28B945070033E859 /* RadioSwatch */ = {
isa = PBXGroup;
children = (
@ -261,7 +275,7 @@
isa = PBXGroup;
children = (
EA4DB2FE28DCBC1900103EE3 /* Badge */,
5FC35BE128D513EB004EBEAC /* Button */,
EA0FC2BE2912D18200DF80B4 /* Buttons */,
EAF7F092289985E200B287F5 /* Checkbox */,
EA3362412892EF700071C351 /* Label */,
EA89200B28B530F0006B9984 /* RadioBox */,
@ -411,6 +425,22 @@
path = Publishers;
sourceTree = "<group>";
};
EAC9257E29119B5D00091998 /* TextLink */ = {
isa = PBXGroup;
children = (
EAC9257C29119B5400091998 /* TextLink.swift */,
);
path = TextLink;
sourceTree = "<group>";
};
EAC925812911B34300091998 /* TextLinkCaret */ = {
isa = PBXGroup;
children = (
EAC925822911B35300091998 /* TextLinkCaret.swift */,
);
path = TextLinkCaret;
sourceTree = "<group>";
};
EAC925852911C9DE00091998 /* TextFields */ = {
isa = PBXGroup;
children = (
@ -594,6 +624,7 @@
EAC9258C2911C9DE00091998 /* TextEntryField.swift in Sources */,
EA3362402892EF6C0071C351 /* Label.swift in Sources */,
EAF7F0B3289B1ADC00B287F5 /* ActionLabelAttribute.swift in Sources */,
EAC925832911B35400091998 /* TextLinkCaret.swift in Sources */,
EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */,
EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */,
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */,
@ -641,6 +672,7 @@
EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */,
EA3361BF288B2EA60071C351 /* Handlerable.swift in Sources */,
EA3361A8288B23300071C351 /* UIColor.swift in Sources */,
EAC9257D29119B5400091998 /* TextLink.swift in Sources */,
EA1F266628B945070033E859 /* RadioSwatchGroup.swift in Sources */,
5FC35BE328D51405004EBEAC /* Button.swift in Sources */,
);

View File

@ -0,0 +1,103 @@
//
// TextLink.swift
// VDS
//
// Created by Matt Bruce on 11/1/22.
//
import Foundation
import UIKit
import VDSColorTokens
import VDSFormControlsTokens
import Combine
@objc(VDSTextLink)
open class TextLink: Control {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var heightConstraint: NSLayoutConstraint?
private var label = Label()
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var text: String? { didSet { didChange() } }
open var size: ButtonSize = .large { didSet { didChange() }}
private var height: CGFloat {
switch size {
case .large:
return 44
case .small:
return 32
}
}
//--------------------------------------------------
// 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 initialSetup() {
super.initialSetup()
}
open override func setup() {
super.setup()
addSubview(label)
//add tapGesture to self
publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in
self?.sendActions(for: .touchUpInside)
}.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
heightConstraint = heightAnchor.constraint(equalToConstant: height)
heightConstraint?.isActive = true
}
open override func reset() {
super.reset()
size = .large
accessibilityCustomActions = []
accessibilityTraits = .staticText
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func updateView() {
label.surface = surface
label.disabled = disabled
label.typograpicalStyle = size == .large ? TypographicalStyle.BodyLarge : TypographicalStyle.BodySmall
label.text = text ?? ""
label.attributes = [UnderlineLabelAttribute(location: 0, length: label.text!.count)]
heightConstraint?.constant = height
}
}

View File

@ -0,0 +1,304 @@
//
// TextLinkCaret.swift
// VDS
//
// Created by Matt Bruce on 11/1/22.
//
import Foundation
import UIKit
import VDSColorTokens
import VDSFormControlsTokens
import Combine
public enum TextLinkCaretPosition: String, CaseIterable {
case left, right
}
@objc(VDSTextLinkCaret)
open class TextLinkCaret: Control {
//--------------------------------------------------
// MARK: - Private Properties
//--------------------------------------------------
private var heightConstraint: NSLayoutConstraint?
private var label = Label().with {
$0.typograpicalStyle = TypographicalStyle.BoldBodyLarge
}
private var caretView = CaretView().with {
$0.size = CaretView.CaretSize.small(.vertical)
$0.lineWidth = 2
}
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
open var text: String? { didSet { didChange() } }
open var iconPosition: TextLinkCaretPosition = .right { didSet { didChange() } }
private var height: CGFloat {
44
}
//--------------------------------------------------
// 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 initialSetup() {
super.initialSetup()
}
open override func setup() {
super.setup()
//add tapGesture to self
publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in
self?.sendActions(for: .touchUpInside)
}.store(in: &subscribers)
//constraints
heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true
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
}
//--------------------------------------------------
// MARK: - Constraints
//--------------------------------------------------
private var caretLeadingConstraint: NSLayoutConstraint?
private var caretTrailingConstraint: NSLayoutConstraint?
private var labelConstraint: NSLayoutConstraint?
open override func reset() {
super.reset()
accessibilityCustomActions = []
accessibilityTraits = .staticText
}
//--------------------------------------------------
// MARK: - Overrides
//--------------------------------------------------
open override func updateView() {
var updatedText = text ?? ""
caretView.surface = surface
caretView.disabled = disabled
caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left
let image = caretView.getImage()
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
label.text = iconPosition == .right ? "\(updatedText) " : " \(updatedText)"
label.attributes = [imageAttribute]
}
}
extension UIView {
public func getImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
let image = renderer.image { ctx in
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
}
return image
}
}
internal class CaretView: View {
//------------------------------------------------------
// MARK: - Properties
//------------------------------------------------------
private var caretPath: UIBezierPath = UIBezierPath()
public var lineWidth: CGFloat = 1 { didSet{ didChange() } }
public var direction: Direction = .right { didSet{ didChange() } }
public var size: CaretSize? { didSet{ didChange() } }
public var colorConfiguration: AnyColorable = DisabledSurfaceColorConfiguration().with {
$0.disabled.lightColor = VDSColor.elementsSecondaryOnlight
$0.disabled.darkColor = VDSColor.elementsSecondaryOndark
$0.enabled.lightColor = VDSColor.elementsPrimaryOnlight
$0.enabled.darkColor = VDSColor.elementsPrimaryOndark
}.eraseToAnyColorable()
//------------------------------------------------------
// MARK: - Constraints
//------------------------------------------------------
/// Sizes of CaretView are derived from InVision design specs. They are provided for convenience.
public enum CaretSize {
case small(Orientation)
case medium(Orientation)
case large(Orientation)
/// Orientation based on the longest line of the view.
public enum Orientation {
case vertical
case horizontal
}
/// Dimensions of container; provided by InVision design.
func dimensions() -> CGSize {
switch self {
case .small(let o):
return o == .vertical ? CGSize(width: 6.9, height: 10.96) : CGSize(width: 10.96, height: 6.9)
case .medium(let o):
return o == .vertical ? CGSize(width: 9.9, height: 16.96) : CGSize(width: 16.96, height: 9.9)
case .large(let o):
return o == .vertical ? CGSize(width: 14.9, height: 24.96) : CGSize(width: 24.96, height: 14.9)
}
}
}
//------------------------------------------------------
// MARK: - Initialization
//------------------------------------------------------
public override init(frame: CGRect) {
super.init(frame: frame)
}
public convenience init(lineWidth: CGFloat) {
self.init(frame: .zero)
self.lineWidth = lineWidth
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("CaretView xib not supported.")
}
required public convenience init() {
self.init(frame: .zero)
}
public convenience init(size: CaretSize){
let dimensions = size.dimensions()
self.init(frame: .init(x: 0, y: 0, width: dimensions.width, height: dimensions.height))
self.size = size
}
//------------------------------------------------------
// MARK: - Setup
//------------------------------------------------------
override open func setup() {
super.setup()
defaultState()
}
//------------------------------------------------------
// MARK: - Drawing
//------------------------------------------------------
/// The direction the caret will be pointing to.
public enum Direction: Int {
case left
case right
case down
case up
}
override func draw(_ rect: CGRect) {
super.draw(rect)
caretPath.removeAllPoints()
caretPath.lineJoinStyle = .miter
caretPath.lineWidth = lineWidth
let inset = lineWidth / 2
let halfWidth = frame.size.width / 2
let halfHeight = frame.size.height / 2
switch direction {
case .up:
caretPath.move(to: CGPoint(x: inset, y: frame.size.height - inset))
caretPath.addLine(to: CGPoint(x: halfWidth, y: inset))
caretPath.addLine(to: CGPoint(x: frame.size.width, y: frame.size.height))
case .right:
caretPath.move(to: CGPoint(x: inset, y: inset))
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: halfHeight))
caretPath.addLine(to: CGPoint(x: inset, y: frame.size.height - inset))
case .down:
caretPath.move(to: CGPoint(x: inset, y: inset))
caretPath.addLine(to: CGPoint(x: halfWidth, y: frame.size.height - inset))
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: inset))
case .left:
caretPath.move(to: CGPoint(x: frame.size.width - inset, y: inset))
caretPath.addLine(to: CGPoint(x: inset, y: halfHeight))
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: frame.size.height - inset))
}
let color = colorConfiguration.getColor(self)
color.setStroke()
caretPath.stroke()
}
override func updateView() {
setNeedsDisplay()
}
//------------------------------------------------------
// MARK: - Methods
//------------------------------------------------------
public func setLineColor(_ color: UIColor) {
setNeedsDisplay()
}
public func defaultState() {
isOpaque = false
isHidden = false
backgroundColor = .clear
}
/// Ensure you have defined a CaretSize with Orientation before calling.
public func setConstraints() {
guard let dimensions = size?.dimensions() else { return }
heightAnchor.constraint(equalToConstant: dimensions.height).isActive = true
widthAnchor.constraint(equalToConstant: dimensions.width).isActive = true
}
}

View File

@ -12,11 +12,13 @@ public struct ImageLabelAttribute: AttachmentLabelAttributeModel {
public enum Error: Swift.Error {
case bundleNotFound
case imageNotFound(String)
case imageNotSet
}
public var id = UUID()
public var location: Int
public var length: Int
public var imageName: String
public var imageName: String?
public var image: UIImage?
public var frame: CGRect
public var tintColor: UIColor
public static func == (lhs: ImageLabelAttribute, rhs: ImageLabelAttribute) -> Bool {
@ -27,18 +29,34 @@ public struct ImageLabelAttribute: AttachmentLabelAttributeModel {
return id == equatable.id && range == equatable.range && imageName == equatable.imageName
}
public func getAttachment() throws -> NSTextAttachment {
guard let bundle = Bundle(identifier: "com.vzw.vds") else {
throw Error.bundleNotFound
}
guard let image = UIImage(named: imageName, in: bundle, with: nil) else {
throw Error.imageNotFound(imageName)
}
private func imageAttachment(image: UIImage) -> NSTextAttachment {
let attachment = NSTextAttachment()
attachment.image = image.withTintColor(tintColor)
attachment.bounds = frame
return attachment
}
public func getAttachment() throws -> NSTextAttachment {
//get a local asset
if let imageName {
guard let bundle = Bundle(identifier: "com.vzw.vds") else {
throw Error.bundleNotFound
}
guard let image = UIImage(named: imageName, in: bundle, with: nil) else {
throw Error.imageNotFound(imageName)
}
return imageAttachment(image: image)
} //get from set image
else if let image {
return imageAttachment(image: image)
} else {
throw Error.imageNotSet
}
}
}