Compare commits
1 Commits
develop
...
feature/la
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c4697f29a |
@ -67,6 +67,7 @@
|
|||||||
EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C662970C21600F2FF2E /* VDSLayout.swift */; };
|
EA985C672970C21600F2FF2E /* VDSLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C662970C21600F2FF2E /* VDSLayout.swift */; };
|
||||||
EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C682971B90B00F2FF2E /* IconSize.swift */; };
|
EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C682971B90B00F2FF2E /* IconSize.swift */; };
|
||||||
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA985C7C297DAED300F2FF2E /* Primitive.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 */; };
|
EAA5EEB528ECBFB4003B3210 /* ImageLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB428ECBFB4003B3210 /* ImageLabelAttribute.swift */; };
|
||||||
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */; };
|
EAA5EEB728ECC03A003B3210 /* ToolTipLabelAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA5EEB628ECC03A003B3210 /* ToolTipLabelAttribute.swift */; };
|
||||||
EAA5EEB928ECD24B003B3210 /* Icons.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EAA5EEB828ECD24B003B3210 /* Icons.xcassets */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
EAA5EEB828ECD24B003B3210 /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = "<group>"; };
|
||||||
@ -315,6 +317,7 @@
|
|||||||
EA33619D288B1E330071C351 /* Components */,
|
EA33619D288B1E330071C351 /* Components */,
|
||||||
EA3361A6288B23240071C351 /* Extensions */,
|
EA3361A6288B23240071C351 /* Extensions */,
|
||||||
EA3361DF2891D0F10071C351 /* Fonts */,
|
EA3361DF2891D0F10071C351 /* Fonts */,
|
||||||
|
EA985C8C2983376100F2FF2E /* Managers */,
|
||||||
EA3361AB288B25EC0071C351 /* Protocols */,
|
EA3361AB288B25EC0071C351 /* Protocols */,
|
||||||
EAB1D2E228AE842000DAE764 /* Publishers */,
|
EAB1D2E228AE842000DAE764 /* Publishers */,
|
||||||
EAB1D2D028ABEF3100DAE764 /* Typography */,
|
EAB1D2D028ABEF3100DAE764 /* Typography */,
|
||||||
@ -415,7 +418,6 @@
|
|||||||
EA3361B4288B2A360071C351 /* Classes */ = {
|
EA3361B4288B2A360071C351 /* Classes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
|
|
||||||
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
|
EAF7F0B8289C139800B287F5 /* ColorConfiguration.swift */,
|
||||||
EAF7F09D289AAEC000B287F5 /* Constants.swift */,
|
EAF7F09D289AAEC000B287F5 /* Constants.swift */,
|
||||||
EA3361B5288B2A410071C351 /* Control.swift */,
|
EA3361B5288B2A410071C351 /* Control.swift */,
|
||||||
@ -541,6 +543,15 @@
|
|||||||
path = TextArea;
|
path = TextArea;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
EA985C8C2983376100F2FF2E /* Managers */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
EA985C1C296CD13600F2FF2E /* BundleManager.swift */,
|
||||||
|
EA985C8D2983377600F2FF2E /* ImageManager.swift */,
|
||||||
|
);
|
||||||
|
path = Managers;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
EAB1D2D028ABEF3100DAE764 /* Typography */ = {
|
EAB1D2D028ABEF3100DAE764 /* Typography */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -804,6 +815,7 @@
|
|||||||
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
|
EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */,
|
||||||
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */,
|
EA985C7D297DAED300F2FF2E /* Primitive.swift in Sources */,
|
||||||
EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */,
|
EAB5FEF829393A7200998C17 /* ButtonGroupConstants.swift in Sources */,
|
||||||
|
EA985C8E2983377600F2FF2E /* ImageManager.swift in Sources */,
|
||||||
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */,
|
EA3361AF288B26310071C351 /* FormFieldable.swift in Sources */,
|
||||||
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */,
|
EA5E3058295105A40082B959 /* Tilelet.swift in Sources */,
|
||||||
EA5E304E294CC7F00082B959 /* VDSColor.swift in Sources */,
|
EA5E304E294CC7F00082B959 /* VDSColor.swift in Sources */,
|
||||||
|
|||||||
@ -49,9 +49,13 @@ public struct ActionLabelAttribute: ActionLabelAttributeModel {
|
|||||||
case location, length
|
case location, length
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setAttribute(on attributedString: NSMutableAttributedString) {
|
public func setAttribute(on attributedString: NSMutableAttributedString) {}
|
||||||
if(shouldUnderline){
|
}
|
||||||
UnderlineLabelAttribute(location: location, length: length).setAttribute(on: attributedString)
|
|
||||||
|
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) {
|
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
|
//set letterSpacing
|
||||||
if textStyle.letterSpacing > 0.0 {
|
if attribute.textStyle.letterSpacing > 0.0 {
|
||||||
attributedString.removeAttribute(.kern, range: range)
|
attributedString.removeAttribute(.kern, range: attribute.range)
|
||||||
attributedString.addAttribute(.kern, value: textStyle.letterSpacing, range: range)
|
attributedString.addAttribute(.kern, value: attribute.textStyle.letterSpacing, range: attribute.range)
|
||||||
}
|
}
|
||||||
|
|
||||||
//set lineHeight
|
//set lineHeight
|
||||||
if textStyle.lineHeight > 0.0 {
|
if attribute.textStyle.lineHeight > 0.0 {
|
||||||
let lineHeight = textStyle.lineHeight
|
let lineHeight = attribute.textStyle.lineHeight
|
||||||
let adjustment = lineHeight > textStyle.font.lineHeight ? 2.0 : 1.0
|
let adjustment = lineHeight > attribute.textStyle.font.lineHeight ? 2.0 : 1.0
|
||||||
let baselineOffset = (lineHeight - textStyle.font.lineHeight) / 2.0 / adjustment
|
let baselineOffset = (lineHeight - attribute.textStyle.font.lineHeight) / 2.0 / adjustment
|
||||||
let paragraph = NSMutableParagraphStyle().with {
|
let paragraph = NSMutableParagraphStyle().with {
|
||||||
$0.maximumLineHeight = lineHeight
|
$0.maximumLineHeight = lineHeight
|
||||||
$0.minimumLineHeight = lineHeight
|
$0.minimumLineHeight = lineHeight
|
||||||
$0.alignment = textPosition.textAlignment
|
$0.alignment = attribute.textPosition.textAlignment
|
||||||
$0.lineBreakMode = lineBreakMode
|
$0.lineBreakMode = attribute.lineBreakMode
|
||||||
}
|
}
|
||||||
attributedString.removeAttribute(.baselineOffset, range: range)
|
attributedString.removeAttribute(.baselineOffset, range: attribute.range)
|
||||||
attributedString.removeAttribute(.paragraphStyle, range: range)
|
attributedString.removeAttribute(.paragraphStyle, range: attribute.range)
|
||||||
attributedString.addAttribute(.baselineOffset, value: baselineOffset, range: range)
|
attributedString.addAttribute(.baselineOffset, value: baselineOffset, range: attribute.range)
|
||||||
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: range)
|
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: attribute.range)
|
||||||
|
|
||||||
} else if textPosition != .left {
|
} else if attribute.textPosition != .left {
|
||||||
let paragraph = NSMutableParagraphStyle().with {
|
let paragraph = NSMutableParagraphStyle().with {
|
||||||
$0.alignment = textPosition.textAlignment
|
$0.alignment = attribute.textPosition.textAlignment
|
||||||
$0.lineBreakMode = lineBreakMode
|
$0.lineBreakMode = attribute.lineBreakMode
|
||||||
}
|
}
|
||||||
attributedString.removeAttribute(.paragraphStyle, range: range)
|
attributedString.removeAttribute(.paragraphStyle, range: attribute.range)
|
||||||
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: range)
|
attributedString.addAttribute(.paragraphStyle, value: paragraph, range: attribute.range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,12 +52,7 @@ public struct UnderlineLabelAttribute: LabelAttributeModel {
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Public Functions
|
// MARK: - Public Functions
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
public func setAttribute(on attributedString: NSMutableAttributedString) {
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UnderlineLabelAttribute {
|
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
|
//create the primary string
|
||||||
let startingAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor]
|
let startingAttributes = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor]
|
||||||
let mutableText = NSMutableAttributedString(string: text, attributes: startingAttributes)
|
let mutableText = NSMutableAttributedString(string: text, attributes: startingAttributes)
|
||||||
|
|
||||||
//set the local lineHeight/lineSpacing attributes
|
|
||||||
setStyleAttributes(attributedString: mutableText)
|
|
||||||
|
|
||||||
applyAttributes(mutableText)
|
applyAttributes(mutableText)
|
||||||
|
|
||||||
//set the attributed text
|
//set the attributed text
|
||||||
@ -147,21 +144,40 @@ public class Label: UILabel, Handlerable, ViewProtocol, Resettable, UserInfoable
|
|||||||
private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) {
|
private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) {
|
||||||
actions = []
|
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 {
|
if let attributes = attributes {
|
||||||
//loop through the models attributes
|
allAttributes.append(contentsOf: attributes)
|
||||||
for attribute in 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
|
//create a wrapper for the attributes range, block and
|
||||||
attribute.setAttribute(on: mutableAttributedString)
|
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