initial refactor

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2023-01-27 10:47:36 -06:00
parent 2394055815
commit 7c4697f29a
7 changed files with 238 additions and 51 deletions

View File

@ -67,6 +67,7 @@
EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C662970C21600F2FF2E /* VDSLayout.swift */; };
EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C682971B90B00F2FF2E /* IconSize.swift */; };
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C7C297DAED300F2FF2E /* Primitive.swift */; };
EA985C8E2983377600F2FF2E /* ImageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C8D2983377600F2FF2E /* ImageManager.swift */; };
EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */; };
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */; };
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEB828ECD24B003B3210 /* Icons.xcassets */; };
@ -183,6 +184,7 @@
EA985C662970C21600F2FF2E /* VDSLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSLayout.swift; sourceTree = "<group>"; };
EA985C682971B90B00F2FF2E /* IconSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSize.swift; sourceTree = "<group>"; };
EA985C7C297DAED300F2FF2E /* Primitive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Primitive.swift; sourceTree = "<group>"; };
EA985C8D2983377600F2FF2E /* ImageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageManager.swift; sourceTree = "<group>"; };
EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLabelAttribute.swift; sourceTree = "<group>"; };
EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolTipLabelAttribute.swift; sourceTree = "<group>"; };
EAA5EEB828ECD24B003B3210 /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = "<group>"; };
@ -315,6 +317,7 @@
EA33619D288B1E330071C351 /* Components */,
EA3361A6288B23240071C351 /* Extensions */,
EA3361DF2891D0F10071C351 /* Fonts */,
EA985C8C2983376100F2FF2E /* Managers */,
EA3361AB288B25EC0071C351 /* Protocols */,
EAB1D2E228AE842000DAE764 /* Publishers */,
EAB1D2D028ABEF3100DAE764 /* Typography */,
@ -415,7 +418,6 @@
EA3361B4288B2A360071C351 /* Classes */ = {
isa = PBXGroup;
children = (
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
EAF7F09D289AAEC000B287F5 /* Constants.swift */,
EA3361B5288B2A410071C351 /* Control.swift */,
@ -541,6 +543,15 @@
path = TextArea;
sourceTree = "<group>";
};
EA985C8C2983376100F2FF2E /* Managers */ = {
isa = PBXGroup;
children = (
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
EA985C8D2983377600F2FF2E /* ImageManager.swift */,
);
path = Managers;
sourceTree = "<group>";
};
EAB1D2D028ABEF3100DAE764 /* Typography */ = {
isa = PBXGroup;
children = (
@ -804,6 +815,7 @@
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */,
EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */,
EA985C8E2983377600F2FF2E /* ImageManager.swift in Sources */,
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */,
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */,
EA5E304E294CC7F00082B959 /* VDSColor.swift in Sources */,

View File

@ -49,9 +49,13 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel {
case location, length
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
if(shouldUnderline){
UnderlineLabelAttribute(location: location, length: length).setAttribute(on: attributedString)
public func setAttribute(on attributedString: NSMutableAttributedString) {}
}
extension Label {
internal func apply(attribute: ActionLabelAttribute, on attributedString: NSMutableAttributedString) {
if(attribute.shouldUnderline){
apply(attribute: .init(location: attribute.location, length: attribute.length), on: attributedString)
}
}
}

View File

@ -44,43 +44,45 @@ public struct TextStyleLabelAttribute: LabelAttributeModel {
}
public func setAttribute(on attributedString: NSMutableAttributedString) {
attributedString.removeAttribute(.font, range: range)
attributedString.removeAttribute(.foregroundColor, range: range)
attributedString.addAttribute(.font, value: textStyle.font, range: range)
attributedString.addAttribute(.foregroundColor, value: textColor, range: range)
setStyleAttributes(attributedString)
}
private func setStyleAttributes(_ attributedString: NSMutableAttributedString) {
}
extension Label {
internal func apply(attribute: TextStyleLabelAttribute, on attributedString: NSMutableAttributedString){
attributedString.removeAttribute(.font, range: attribute.range)
attributedString.removeAttribute(.foregroundColor, range: attribute.range)
attributedString.addAttribute(.font, value: attribute.textStyle.font, range: attribute.range)
attributedString.addAttribute(.foregroundColor, value: attribute.textColor, range: attribute.range)
//set letterSpacing
if textStyle.letterSpacing > 0.0 {
attributedString.removeAttribute(.kern, range: range)
attributedString.addAttribute(.kern, value: textStyle.letterSpacing, range: range)
if attribute.textStyle.letterSpacing > 0.0 {
attributedString.removeAttribute(.kern, range: attribute.range)
attributedString.addAttribute(.kern, value: attribute.textStyle.letterSpacing, range: attribute.range)
}
//set lineHeight
if textStyle.lineHeight > 0.0 {
let lineHeight = textStyle.lineHeight
let adjustment = lineHeight > textStyle.font.lineHeight ? 2.0 : 1.0
let baselineOffset = (lineHeight - textStyle.font.lineHeight) / 2.0 / adjustment
if attribute.textStyle.lineHeight > 0.0 {
let lineHeight = attribute.textStyle.lineHeight
let adjustment = lineHeight > attribute.textStyle.font.lineHeight ? 2.0 : 1.0
let baselineOffset = (lineHeight - attribute.textStyle.font.lineHeight) / 2.0 / adjustment
let paragraph = NSMutableParagraphStyle().with {
$0.maximumLineHeight = lineHeight
$0.minimumLineHeight = lineHeight
$0.alignment = textPosition.textAlignment
$0.lineBreakMode = lineBreakMode
$0.alignment = attribute.textPosition.textAlignment
$0.lineBreakMode = attribute.lineBreakMode
}
attributedString.removeAttribute(.baselineOffset, range: range)
attributedString.removeAttribute(.paragraphStyle, range: range)
attributedString.addAttribute(.baselineOffset, value: baselineOffset, range: range)
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: range)
attributedString.removeAttribute(.baselineOffset, range: attribute.range)
attributedString.removeAttribute(.paragraphStyle, range: attribute.range)
attributedString.addAttribute(.baselineOffset, value: baselineOffset, range: attribute.range)
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: attribute.range)
} else if textPosition != .left {
} else if attribute.textPosition != .left {
let paragraph = NSMutableParagraphStyle().with {
$0.alignment = textPosition.textAlignment
$0.lineBreakMode = lineBreakMode
$0.alignment = attribute.textPosition.textAlignment
$0.lineBreakMode = attribute.lineBreakMode
}
attributedString.removeAttribute(.paragraphStyle, range: range)
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: range)
attributedString.removeAttribute(.paragraphStyle, range: attribute.range)
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: attribute.range)
}
}
}

View File

@ -52,12 +52,7 @@ public struct UnderlineLabelAttribute: LabelAttributeModel {
//--------------------------------------------------
// MARK: - Public Functions
//--------------------------------------------------
public func setAttribute(on attributedString: NSMutableAttributedString) {
attributedString.addAttribute(.underlineStyle, value: underlineValue.rawValue, range: range)
if let color = color {
attributedString.addAttribute(.underlineColor, value: color, range: range)
}
}
public func setAttribute(on attributedString: NSMutableAttributedString) {}
}
extension UnderlineLabelAttribute {
@ -114,3 +109,12 @@ extension UnderlineLabelAttribute {
}
}
}
extension Label {
internal func apply(attribute: UnderlineLabelAttribute, on attributedString: NSMutableAttributedString) {
attributedString.addAttribute(.underlineStyle, value: attribute.underlineValue.rawValue, range: attribute.range)
if let color = attribute.color {
attributedString.addAttribute(.underlineColor, value: color, range: attribute.range)
}
}
}

View File

@ -131,10 +131,7 @@ public class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable
//create the primary string
let startingAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor]
let mutableText = NSMutableAttributedString(string: text, attributes: startingAttributes)
//set the local lineHeight/lineSpacing attributes
setStyleAttributes(attributedString: mutableText)
applyAttributes(mutableText)
//set the attributed text
@ -147,21 +144,40 @@ public class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable
private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) {
actions = []
//apply the base TextStyle to the entire string
var allAttributes: [any LabelAttributeModel] = [
TextStyleLabelAttribute(location: 0,
length: mutableAttributedString.length,
textStyle: textStyle,
textColor: textColor,
textPosition: textPosition,
lineBreakMode: lineBreakMode),
]
if let attributes = attributes {
//loop through the models attributes
for attribute in attributes {
allAttributes.append(contentsOf: attributes)
}
//loop through the models attributes
for attribute in allAttributes {
//add attribute on the string
attribute.setAttribute(on: mutableAttributedString)
//see if the attribute is Actionable
if let actionable = attribute as? any ActionLabelAttributeModel{
//create a accessibleAction
let customAccessibilityAction = customAccessibilityAction(range: actionable.range, accessibleText: actionable.accessibleText)
//add attribute on the string
attribute.setAttribute(on: mutableAttributedString)
//create a wrapper for the attributes range, block and
actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1))
} else if let attribute = attribute as? TextStyleLabelAttribute {
apply(attribute: attribute, on: mutableAttributedString)
} else if let attribute = attribute as? UnderlineLabelAttribute {
apply(attribute: attribute, on: mutableAttributedString)
//see if the attribute is Actionable
if let actionable = attribute as? any ActionLabelAttributeModel{
//create a accessibleAction
let customAccessibilityAction = customAccessibilityAction(range: actionable.range, accessibleText: actionable.accessibleText)
//create a wrapper for the attributes range, block and
actions.append(LabelAction(range: actionable.range, action: actionable.action, accessibilityID: customAccessibilityAction?.hashValue ?? -1))
}
}
}
}

View File

@ -0,0 +1,149 @@
//
// ImageManager.swift
// VDS
//
// Created by Matt Bruce on 1/26/23.
//
import Foundation
import UIKit.UIImage
import Combine
// Declares in-memory image cache
public protocol ImageCacheType: class {
// Returns the image associated with a given url
func image(for url: URL) -> UIImage?
// Inserts the image of the specified url in the cache
func insertImage(_ image: UIImage?, for url: URL)
// Removes the image of the specified url in the cache
func removeImage(for url: URL)
// Removes all images from the cache
func removeAllImages()
// Accesses the value associated with the given key for reading and writing
subscript(_ url: URL) -> UIImage? { get set }
}
public final class ImageCache: ImageCacheType {
// 1st level cache, that contains encoded images
private lazy var imageCache: NSCache<AnyObject, AnyObject> = {
let cache = NSCache<AnyObject, AnyObject>()
cache.countLimit = config.countLimit
return cache
}()
// 2nd level cache, that contains decoded images
private lazy var decodedImageCache: NSCache<AnyObject, AnyObject> = {
let cache = NSCache<AnyObject, AnyObject>()
cache.totalCostLimit = config.memoryLimit
return cache
}()
private let lock = NSLock()
private let config: Config
public struct Config {
public let countLimit: Int
public let memoryLimit: Int
public static let defaultConfig = Config(countLimit: 100, memoryLimit: 1024 * 1024 * 100) // 100 MB
}
public init(config: Config = Config.defaultConfig) {
self.config = config
}
public func image(for url: URL) -> UIImage? {
lock.lock(); defer { lock.unlock() }
// the best case scenario -> there is a decoded image in memory
if let decodedImage = decodedImageCache.object(forKey: url as AnyObject) as? UIImage {
return decodedImage
}
// search for image data
if let image = imageCache.object(forKey: url as AnyObject) as? UIImage {
let decodedImage = image.decodedImage()
decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decodedImage.diskSize)
return decodedImage
}
return nil
}
public func insertImage(_ image: UIImage?, for url: URL) {
guard let image = image else { return removeImage(for: url) }
let decompressedImage = image.decodedImage()
lock.lock(); defer { lock.unlock() }
imageCache.setObject(decompressedImage, forKey: url as AnyObject, cost: 1)
decodedImageCache.setObject(image as AnyObject, forKey: url as AnyObject, cost: decompressedImage.diskSize)
}
public func removeImage(for url: URL) {
lock.lock(); defer { lock.unlock() }
imageCache.removeObject(forKey: url as AnyObject)
decodedImageCache.removeObject(forKey: url as AnyObject)
}
public func removeAllImages() {
lock.lock(); defer { lock.unlock() }
imageCache.removeAllObjects()
decodedImageCache.removeAllObjects()
}
public subscript(_ key: URL) -> UIImage? {
get {
return image(for: key)
}
set {
return insertImage(newValue, for: key)
}
}
}
fileprivate extension UIImage {
func decodedImage() -> UIImage {
guard let cgImage = cgImage else { return self }
let size = CGSize(width: cgImage.width, height: cgImage.height)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: cgImage.bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)
context?.draw(cgImage, in: CGRect(origin: .zero, size: size))
guard let decodedImage = context?.makeImage() else { return self }
return UIImage(cgImage: decodedImage)
}
// Rough estimation of how much memory image uses in bytes
var diskSize: Int {
guard let cgImage = cgImage else { return 0 }
return cgImage.bytesPerRow * cgImage.height
}
}
public final class ImageLoader {
public static let shared = ImageLoader()
private let cache: ImageCacheType
private lazy var backgroundQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 5
return queue
}()
public init(cache: ImageCacheType = ImageCache()) {
self.cache = cache
}
public func loadImage(from url: URL) -> AnyPublisher<UIImage?, Never> {
if let image = cache[url] {
return Just(image).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { (data, response) -> UIImage? in return UIImage(data: data) }
.catch { error in return Just(nil) }
.handleEvents(receiveOutput: {[unowned self] image in
guard let image = image else { return }
self.cache[url] = image
})
.print("Image loading \(url):")
.subscribe(on: backgroundQueue)
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}