initial refactor
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
2394055815
commit
7c4697f29a
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
149
VDS/Managers/ImageManager.swift
Normal file
149
VDS/Managers/ImageManager.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user