轻量化滤镜图像和GIF框架分享到你~

一、背景

日常开发过程中,经常需要在各类型控件上加载显示图像orGIF,例如:UIImageView、UIBtton、NSImageView、NSButton等等。
这时候有个网络图像库就会便利太多太多,很多人这时候会说,对于这块其实网络上有很多,比如 KingfisherYYWebImage等等。

0 0. 框架由来,

  • 本来之前呢只是想实现一个如何播放GIF,于是乎就出现第一版对任意控件实现播放GIF功能,这边只需要支持 AsAnimatable 即可快速达到支持播放GIF功能;
  • 后面Boss居然又说需要对GIF图支持注入滤镜功能,于是乎又修改底层,对播放的GIF图实现滤镜功能,于是之前写的滤镜库 Harbeth 即将闪亮登场;
  • 然后Boss又说,首页banner需要图像和GIF混合显示,索性就又来简单封装显示网络图像,然后根据 AssetType 来区分是属于网图还是GIF图,以达到混合现实网络图像和网络GIF图以及本地图像和本地GIF混合播放功能;
  • 起初也只是简单的去下载资源Data用于显示图像,这时候boss又要搞事情了,图像显示有点慢,于是乎又开始写网络下载模块 DataDownloader 和磁盘缓存模块 Cached ,对于已下载的图像存储于磁盘缓存方便后续再次显示,同样的网络链接地址同时下载时不会重复下载,下载完成后统一分发响应,对于下载部分资源进行断点续载功能;
  • 慢慢越写越发现这玩意不就是一个图像库嘛,so 它就这么的孕育而生了!!!

备注:作为参考对象,当然这里面会有一些 Kingfisher 的影子,so 再次感谢猫神!!也学到不少新东西,Thanks!

先贴地址:github.com/yangKJ/Imag…

图片说明

实现方案

这边主要就是分为以下几大模块,网络下载模块资源缓存模块GIF播放模块控件展示模块 以及 配置模块等;

这边对于资源缓存模块,已独立封装成库 Lemons 来使用,支持磁盘和内存存储,同时也会对磁盘数据进行时间过期和达到最大缓存空间的自动清理。

如何播放GIF

对于这块,核心其实就是使用CADisplayLink不断刷新和更新GIF帧图,然后对不同的控件去设置显示图像资源;

主要就是针对不同对象设置显示内容:

extension AsAnimatable {
/// Setting up what is currently showing.
@inline(__always) func setContentImage(_ image: C7Image?, other: ImageX.Others?) {
switch self {
case var container_ as ImageContainer:
container_.image = image
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
case var container_ as NSButtonContainer:
guard let other = other else {
return
}
switch Others.NSButtonKey(rawValue: other.key) {
case .none:
break
case .image:
container_.image = image
case .alternateImage:
container_.alternateImage = image
}
#endif
#if canImport(UIKit) && !os(watchOS)
case var container_ as UIButtonContainer:
guard let other = other else {
return
}
switch Others.UIButtonKey(rawValue: other.key) {
case .none:
break
case .image:
if let state = other.value as? UIControl.State {
container_.setImage(image, for: state)
let (_, backImage) = container_.cacheImages[state.rawValue] ?? (nil, nil)
container_.cacheImages[state.rawValue] = (image, backImage)
}
case .backgroundImage:
if let state = other.value as? UIControl.State {
container_.setBackgroundImage(image, for: state)
let (image_, _) = container_.cacheImages[state.rawValue] ?? (nil, nil)
container_.cacheImages[state.rawValue] = (image_, image)
}
}
case var container_ as UIImageViewContainer:
guard let other = other else {
return
}
switch Others.UIImageViewKey(rawValue: other.key) {
case .none:
break
case .image:
container_.image = image
case .highlightedImage:
container_.highlightedImage = image
}
#endif
#if canImport(WatchKit)
case var container_ as WKInterfaceImageContainer:
container_.image = image
container_.setImage(image)
#endif
default:
#if !os(macOS)
//self.layer.setNeedsDisplay()
self.layer.contents = image?.cgImage
#endif
}
}
}
extension AsAnimatable {
    



    /// Setting up what is currently showing.
    @inline(__always) func setContentImage(_ image: C7Image?, other: ImageX.Others?) {
        switch self {
        case var container_ as ImageContainer:
            container_.image = image
        #if canImport(AppKit) && !targetEnvironment(macCatalyst)
        case var container_ as NSButtonContainer:
            guard let other = other else {
                return
            }
            switch Others.NSButtonKey(rawValue: other.key) {
            case .none:
                break
            case .image:
                container_.image = image
            case .alternateImage:
                container_.alternateImage = image
            }
        #endif
        #if canImport(UIKit) && !os(watchOS)
        case var container_ as UIButtonContainer:
            guard let other = other else {
                return
            }
            switch Others.UIButtonKey(rawValue: other.key) {
            case .none:
                break
            case .image:
                if let state = other.value as? UIControl.State {
                    container_.setImage(image, for: state)
                    let (_, backImage) = container_.cacheImages[state.rawValue] ?? (nil, nil)
                    container_.cacheImages[state.rawValue] = (image, backImage)
                }
            case .backgroundImage:
                if let state = other.value as? UIControl.State {
                    container_.setBackgroundImage(image, for: state)
                    let (image_, _) = container_.cacheImages[state.rawValue] ?? (nil, nil)
                    container_.cacheImages[state.rawValue] = (image_, image)
                }
            }
        case var container_ as UIImageViewContainer:
            guard let other = other else {
                return
            }
            switch Others.UIImageViewKey(rawValue: other.key) {
            case .none:
                break
            case .image:
                container_.image = image
            case .highlightedImage:
                container_.highlightedImage = image
            }
        #endif
        #if canImport(WatchKit)
        case var container_ as WKInterfaceImageContainer:
            container_.image = image
            container_.setImage(image)
        #endif
        default:
            #if !os(macOS)
            //self.layer.setNeedsDisplay()
            self.layer.contents = image?.cgImage
            #endif
        }
    }

}
extension AsAnimatable { /// Setting up what is currently showing. @inline(__always) func setContentImage(_ image: C7Image?, other: ImageX.Others?) { switch self { case var container_ as ImageContainer: container_.image = image #if canImport(AppKit) && !targetEnvironment(macCatalyst) case var container_ as NSButtonContainer: guard let other = other else { return } switch Others.NSButtonKey(rawValue: other.key) { case .none: break case .image: container_.image = image case .alternateImage: container_.alternateImage = image } #endif #if canImport(UIKit) && !os(watchOS) case var container_ as UIButtonContainer: guard let other = other else { return } switch Others.UIButtonKey(rawValue: other.key) { case .none: break case .image: if let state = other.value as? UIControl.State { container_.setImage(image, for: state) let (_, backImage) = container_.cacheImages[state.rawValue] ?? (nil, nil) container_.cacheImages[state.rawValue] = (image, backImage) } case .backgroundImage: if let state = other.value as? UIControl.State { container_.setBackgroundImage(image, for: state) let (image_, _) = container_.cacheImages[state.rawValue] ?? (nil, nil) container_.cacheImages[state.rawValue] = (image_, image) } } case var container_ as UIImageViewContainer: guard let other = other else { return } switch Others.UIImageViewKey(rawValue: other.key) { case .none: break case .image: container_.image = image case .highlightedImage: container_.highlightedImage = image } #endif #if canImport(WatchKit) case var container_ as WKInterfaceImageContainer: container_.image = image container_.setImage(image) #endif default: #if !os(macOS) //self.layer.setNeedsDisplay() self.layer.contents = image?.cgImage #endif } } }

目前已对常用控件实现,

  • UIImageView:imagehighlightedImage
  • NSImageVIew:image
  • UIButton:imagebackgroundImage
  • NSButton:imagealternateImage
  • WKInterfaceImage:image

对于UIView没有上述属性显示,so 这边对layer.contents设置也是同样能达到该效果。

如何下载网络资源

对于网络图像显示,不可获取的就是对于资源的下载。

最开始的简单版,

let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
switch (data, error) {
case (.none, let error):
failed?(error)
case (let data?, _):
DispatchQueue.main.async {
self.displayImage(data: data, filters: filters, options: options)
}
let zipData = options.cacheDataZip.compressed(data: data)
let model = CacheModel(data: zipData)
storager.storeCached(model, forKey: key, options: options.cacheOption)
}
}
task.resume()
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
    switch (data, error) {
    case (.none, let error):
        failed?(error)
    case (let data?, _):
        DispatchQueue.main.async {
            self.displayImage(data: data, filters: filters, options: options)
        }
        let zipData = options.cacheDataZip.compressed(data: data)
        let model = CacheModel(data: zipData)
        storager.storeCached(model, forKey: key, options: options.cacheOption)
    }
}
task.resume()
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in switch (data, error) { case (.none, let error): failed?(error) case (let data?, _): DispatchQueue.main.async { self.displayImage(data: data, filters: filters, options: options) } let zipData = options.cacheDataZip.compressed(data: data) let model = CacheModel(data: zipData) storager.storeCached(model, forKey: key, options: options.cacheOption) } } task.resume()

鉴于boss说的显示有点慢,能优化不。于是开始就对网络下载模块开始优化,网络数据共享和断点续下功能就孕育而生,后续再来补充分片下载功能,进一步提升网络下载速率。

网络共享

  • 对于网络共享,这边其实就是采用一个单例来设计,然后对需要下载的资源和回调响应进行存储,以链接地址md5作为key来管理查找,当数据下载回来之后,分别分发给回调响应即可,同时删除缓存的下载器和回调响应对象;
struct Networking {
typealias DownloadResultBlock = ((Result<DataResult, Error>) -> Void)
typealias DownloadProgressBlock = ((_ currentProgress: CGFloat) -> Void)
static let shared = Networking()
private init() { }
@ImageX.Locked var downloaders = [String: DataDownloader]()
@ImageX.Locked var cacheCallBlocks = [(key: String, block: (download: DownloadResultBlock, progress: DownloadProgressBlock?))]()
/// Add network download data task.
/// - Parameters:
/// - url: The link url.
/// - progressBlock: Network data task download progress.
/// - downloadBlock: Download callback response.
/// - retry: Network max retry count and retry interval.
/// - timeoutInterval: The timeout interval for the request. Defaults to 20.0
/// - interval: Network resource data download progress response interval.
/// - Returns: The data task.
@discardableResult func addDownloadURL(_ url: URL,
progressBlock: DownloadProgressBlock? = nil,
downloadBlock: @escaping DownloadResultBlock,
retry: ImageX.DelayRetry = DelayRetry.max3s,
timeoutInterval: TimeInterval = 20,
interval: TimeInterval = 0.02) -> URLSessionDataTask {
let key = Lemons.CryptoType.md5.encryptedString(with: url.absoluteString)
self.cacheCallBlocks.append((key, (downloadBlock, progressBlock)))
if let downloader = self.downloaders[key] {
return downloader.task
}
var request = URLRequest(url: url, timeoutInterval: timeoutInterval)
request.httpShouldUsePipelining = true
request.cachePolicy = .reloadIgnoringLocalCacheData
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
request.allowsConstrainedNetworkAccess = false
}
let downloader = DataDownloader(request: request, named: key, retry: retry, interval: interval) {
for call in cacheCallBlocks where key == call.key {
switch $0 {
case .downloading(let currentProgress):
let type = AssetType(data: $1)
let rest = DataResult(key: key, url: url, data: $1!, response: $2, type: type, downloadStatus: .downloading)
call.block.progress?(currentProgress)
call.block.download(.success(rest))
case .complete:
let type = AssetType(data: $1)
let rest = DataResult(key: key, url: url, data: $1!, response: $2, type: type, downloadStatus: .complete)
call.block.progress?(1.0)
call.block.download(.success(rest))
case .failed(let error):
call.block.download(.failure(error))
case .finished(let error):
call.block.download(.failure(error))
}
}
switch $0 {
case .complete, .finished:
self.removeDownloadURL(with: key)
case .failed, .downloading:
break
}
}
self.downloaders[key] = downloader
return downloader.task
}
/// Remove the download data task.
/// - Parameter url: The link url.
func removeDownloadURL(with url: URL) {
let key = Lemons.CryptoType.md5.encryptedString(with: url.absoluteString)
removeDownloadURL(with: key)
}
/// No other callbacks waiting, we can clear the task now.
func removeDownloadURL(with key: String) {
self.downloaders[key]?.cancelTask()
self.downloaders.removeValue(forKey: key)
self.cacheCallBlocks.removeAll { $0.key == key }
}
}
struct Networking {
    



    typealias DownloadResultBlock = ((Result<DataResult, Error>) -> Void)
    typealias DownloadProgressBlock = ((_ currentProgress: CGFloat) -> Void)
    
    static let shared = Networking()
    

    private init() { }
    

    @ImageX.Locked var downloaders = [String: DataDownloader]()
    
    @ImageX.Locked var cacheCallBlocks = [(key: String, block: (download: DownloadResultBlock, progress: DownloadProgressBlock?))]()
    
    /// Add network download data task.
    /// - Parameters:
    ///   - url: The link url.
    ///   - progressBlock: Network data task download progress.
    ///   - downloadBlock: Download callback response.
    ///   - retry: Network max retry count and retry interval.
    ///   - timeoutInterval: The timeout interval for the request. Defaults to 20.0
    ///   - interval: Network resource data download progress response interval.
    /// - Returns: The data task.
    @discardableResult func addDownloadURL(_ url: URL,
                                           progressBlock: DownloadProgressBlock? = nil,
                                           downloadBlock: @escaping DownloadResultBlock,
                                           retry: ImageX.DelayRetry = DelayRetry.max3s,
                                           timeoutInterval: TimeInterval = 20,
                                           interval: TimeInterval = 0.02) -> URLSessionDataTask {
        let key = Lemons.CryptoType.md5.encryptedString(with: url.absoluteString)
        self.cacheCallBlocks.append((key, (downloadBlock, progressBlock)))
        if let downloader = self.downloaders[key] {
            return downloader.task
        }
        var request = URLRequest(url: url, timeoutInterval: timeoutInterval)
        request.httpShouldUsePipelining = true
        request.cachePolicy = .reloadIgnoringLocalCacheData
        if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
            request.allowsConstrainedNetworkAccess = false
        }

        let downloader = DataDownloader(request: request, named: key, retry: retry, interval: interval) {
            for call in cacheCallBlocks where key == call.key {
                switch $0 {
                case .downloading(let currentProgress):
                    let type = AssetType(data: $1)
                    let rest = DataResult(key: key, url: url, data: $1!, response: $2, type: type, downloadStatus: .downloading)
                    call.block.progress?(currentProgress)
                    call.block.download(.success(rest))
                case .complete:
                    let type = AssetType(data: $1)
                    let rest = DataResult(key: key, url: url, data: $1!, response: $2, type: type, downloadStatus: .complete)
                    call.block.progress?(1.0)
                    call.block.download(.success(rest))
                case .failed(let error):
                    call.block.download(.failure(error))
                case .finished(let error):
                    call.block.download(.failure(error))
                }
            }
            switch $0 {
            case .complete, .finished:
                self.removeDownloadURL(with: key)
            case .failed, .downloading:
                break
            }
        }
        self.downloaders[key] = downloader
        return downloader.task
    }
    
    /// Remove the download data task.
    /// - Parameter url: The link url.
    func removeDownloadURL(with url: URL) {
        let key = Lemons.CryptoType.md5.encryptedString(with: url.absoluteString)
        removeDownloadURL(with: key)
    }
    
    /// No other callbacks waiting, we can clear the task now.
    func removeDownloadURL(with key: String) {
        self.downloaders[key]?.cancelTask()
        self.downloaders.removeValue(forKey: key)
        self.cacheCallBlocks.removeAll { $0.key == key }
    }
}
struct Networking { typealias DownloadResultBlock = ((Result<DataResult, Error>) -> Void) typealias DownloadProgressBlock = ((_ currentProgress: CGFloat) -> Void) static let shared = Networking() private init() { } @ImageX.Locked var downloaders = [String: DataDownloader]() @ImageX.Locked var cacheCallBlocks = [(key: String, block: (download: DownloadResultBlock, progress: DownloadProgressBlock?))]() /// Add network download data task. /// - Parameters: /// - url: The link url. /// - progressBlock: Network data task download progress. /// - downloadBlock: Download callback response. /// - retry: Network max retry count and retry interval. /// - timeoutInterval: The timeout interval for the request. Defaults to 20.0 /// - interval: Network resource data download progress response interval. /// - Returns: The data task. @discardableResult func addDownloadURL(_ url: URL, progressBlock: DownloadProgressBlock? = nil, downloadBlock: @escaping DownloadResultBlock, retry: ImageX.DelayRetry = DelayRetry.max3s, timeoutInterval: TimeInterval = 20, interval: TimeInterval = 0.02) -> URLSessionDataTask { let key = Lemons.CryptoType.md5.encryptedString(with: url.absoluteString) self.cacheCallBlocks.append((key, (downloadBlock, progressBlock))) if let downloader = self.downloaders[key] { return downloader.task } var request = URLRequest(url: url, timeoutInterval: timeoutInterval) request.httpShouldUsePipelining = true request.cachePolicy = .reloadIgnoringLocalCacheData if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { request.allowsConstrainedNetworkAccess = false } let downloader = DataDownloader(request: request, named: key, retry: retry, interval: interval) { for call in cacheCallBlocks where key == call.key { switch $0 { case .downloading(let currentProgress): let type = AssetType(data: $1) let rest = DataResult(key: key, url: url, data: $1!, response: $2, type: type, downloadStatus: .downloading) call.block.progress?(currentProgress) call.block.download(.success(rest)) case .complete: let type = AssetType(data: $1) let rest = DataResult(key: key, url: url, data: $1!, response: $2, type: type, downloadStatus: .complete) call.block.progress?(1.0) call.block.download(.success(rest)) case .failed(let error): call.block.download(.failure(error)) case .finished(let error): call.block.download(.failure(error)) } } switch $0 { case .complete, .finished: self.removeDownloadURL(with: key) case .failed, .downloading: break } } self.downloaders[key] = downloader return downloader.task } /// Remove the download data task. /// - Parameter url: The link url. func removeDownloadURL(with url: URL) { let key = Lemons.CryptoType.md5.encryptedString(with: url.absoluteString) removeDownloadURL(with: key) } /// No other callbacks waiting, we can clear the task now. func removeDownloadURL(with key: String) { self.downloaders[key]?.cancelTask() self.downloaders.removeValue(forKey: key) self.cacheCallBlocks.removeAll { $0.key == key } } }

断点续下

  • 对于断点续下功能,这边是采用文件 Files 来实时写入存储已下载的资源,下载再下载到同样数据时刻,即先取出上次已经下载数据,然后从该位置再次下载未下载完整的数据资源即可。
final class DataDownloader: NSObject {
enum Disposition {
case downloading(CGFloat)
case complete
case failed(Error)
case finished(Error)
}
private(set) var task: URLSessionDataTask!
private(set) var session: URLSession?
private(set) var retry: DelayRetry
private(set) var request: URLRequest
private(set) var outputStream: OutputStream?
private(set) var lastDate: Date!
/// Downloaded raw data of current task.
private(set) var mutableData: Data!
/// The downloaded part.
private(set) var offset: Int64 = 0
/// Write to the resource file object.
private(set) var files: Files!
/// Network resource data download progress response interval.
private(set) var interval: TimeInterval
typealias DownloadBlock = ((_ state: Disposition, _ data: Data?, _ response: URLResponse?) -> Void)
let completionHandler: DownloadBlock
init(request: URLRequest, named: String, retry: DelayRetry, interval: TimeInterval, completionHandler: @escaping DownloadBlock) {
self.retry = retry
self.completionHandler = completionHandler
self.request = request
self.interval = interval
super.init()
do {
self.files = try Files.init(named: named)
} catch {
self.result(data: nil, response: nil, state: .finished(error))
return
}
self.setupDataTask()
}
deinit {
session?.invalidateAndCancel()
}
func setupDataTask() {
self.reset()
self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
self.task = session?.dataTask(with: request)
self.task.resume()
self.retry.increaseRetryCount()
}
func cancelTask() {
self.session?.invalidateAndCancel()
if self.task.state != .canceling {
self.task.cancel()
}
}
}
extension DataDownloader {
private func reset() {
self.mutableData = Data()
self.lastDate = Date()
self.session?.invalidateAndCancel()
self.offset = self.files.fileCurrentBytes()
if self.offset > 0 {
if let data = self.files.readData() {
self.mutableData.append(data)
let requestRange = String(format: "bytes=%llu-", self.offset)
self.request.addValue(requestRange, forHTTPHeaderField: "Range")
} else {
self.offset = 0
try? self.files.removeFileItem()
}
}
}
private func result(data: Data?, response: URLResponse?, state: Disposition) {
switch state {
case .downloading, .complete:
if let data = data, data.isEmpty == false {
self.completionHandler(state, data, response)
} else {
self.completionHandler(.failed(invalidDataError()), data, response)
}
case .finished:
self.completionHandler(state, data, response)
case .failed:
self.retry.retry(task: task) { [weak self] state_ in
switch state_ {
case .retring:
self?.setupDataTask()
case .stop:
self?.completionHandler(state, data, response)
}
}
}
}
private func didReceiveData(data: Data, dataTask: URLSessionDataTask) {
self.mutableData.append(data)
if canDownloading() {
let receiveBytes = dataTask.countOfBytesReceived + offset
let allBytes = dataTask.countOfBytesExpectedToReceive + offset
let currentProgress = min(max(0, CGFloat(receiveBytes) / CGFloat(allBytes)), 1)
result(data: mutableData, response: dataTask.response, state: .downloading(currentProgress))
}
}
private func canDownloading() -> Bool {
let currentDate = Date()
let time = currentDate.timeIntervalSince(lastDate)
if time >= self.interval {
lastDate = currentDate
return true
}
return false
}
private func hasSuccessCode(_ response: HTTPURLResponse) -> Bool {
switch response.statusCode {
case 200 ..< 300:
return true
default:
return false
}
}
static let domain = "com.condy.ImageX.downloading"
private func statusCodeError(_ statusCode: Int) -> NSError {
let userInfo = [
NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)
]
return NSError(domain: DataDownloader.domain, code: statusCode, userInfo: userInfo)
}
private func invalidHTTPURLResponseError() -> NSError {
let userInfo = [
NSLocalizedDescriptionKey: "Did receive response is not HTTPURLResponse."
]
return NSError(domain: DataDownloader.domain, code: 2002, userInfo: userInfo)
}
private func invalidDataError() -> NSError {
let userInfo = [
NSLocalizedDescriptionKey: "The downloaded data is empty."
]
return NSError(domain: DataDownloader.domain, code: 3003, userInfo: userInfo)
}
}
extension DataDownloader: URLSessionDataDelegate {
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = dataTask.response as? HTTPURLResponse else {
result(data: nil, response: response, state: .failed(invalidHTTPURLResponseError()))
completionHandler(.cancel)
return
}
guard hasSuccessCode(response) else {
result(data: nil, response: response, state: .failed(statusCodeError(response.statusCode)))
completionHandler(.cancel)
return
}
self.outputStream = OutputStream(url: URL(fileURLWithPath: files.path), append: true)
self.outputStream?.open()
if offset == 0 {
var totalBytes = response.expectedContentLength
let data = Data(bytes: &totalBytes, count: MemoryLayout.size(ofValue: totalBytes))
do {
try URL(fileURLWithPath: files.path).mt.setExtendedAttribute(data: data, forName: Files.totalBytesKey)
} catch {
result(data: nil, response: response, state: .failed(error))
completionHandler(.cancel)
return
}
}
completionHandler(.allow)
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.didReceiveData(data: data, dataTask: dataTask)
self.outputStream?.write(Array(data), maxLength: data.count)
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.session?.invalidateAndCancel()
self.outputStream?.close()
guard let response = task.response as? HTTPURLResponse else {
result(data: nil, response: task.response, state: .failed(invalidHTTPURLResponseError()))
return
}
if let error = error {
let state: Disposition = self.retry.exceededRetriedCount() ? .finished(error) : .failed(error)
result(data: nil, response: response, state: state)
} else if hasSuccessCode(response) {
result(data: mutableData, response: response, state: .complete)
try? files.removeFileItem()
} else {
let error = statusCodeError(response.statusCode)
let state: Disposition = self.retry.exceededRetriedCount() ? .finished(error) : .failed(error)
result(data: nil, response: response, state: state)
}
}
}
final class DataDownloader: NSObject {
    



    enum Disposition {
        case downloading(CGFloat)
        case complete
        case failed(Error)
        case finished(Error)
    }
    

    private(set) var task: URLSessionDataTask!
    private(set) var session: URLSession?
    private(set) var retry: DelayRetry
    private(set) var request: URLRequest
    private(set) var outputStream: OutputStream?
    private(set) var lastDate: Date!
    /// Downloaded raw data of current task.
    private(set) var mutableData: Data!
    /// The downloaded part.
    private(set) var offset: Int64 = 0
    /// Write to the resource file object.
    private(set) var files: Files!
    /// Network resource data download progress response interval.
    private(set) var interval: TimeInterval
    

    typealias DownloadBlock = ((_ state: Disposition, _ data: Data?, _ response: URLResponse?) -> Void)
    let completionHandler: DownloadBlock
    
    init(request: URLRequest, named: String, retry: DelayRetry, interval: TimeInterval, completionHandler: @escaping DownloadBlock) {
        self.retry = retry
        self.completionHandler = completionHandler
        self.request = request
        self.interval = interval
        super.init()
        do {
            self.files = try Files.init(named: named)
        } catch {
            self.result(data: nil, response: nil, state: .finished(error))
            return
        }

        self.setupDataTask()
    }
    
    deinit {
        session?.invalidateAndCancel()
    }
    
    func setupDataTask() {
        self.reset()
        self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
        self.task = session?.dataTask(with: request)
        self.task.resume()
        self.retry.increaseRetryCount()
    }

    

    func cancelTask() {
        self.session?.invalidateAndCancel()
        if self.task.state != .canceling {
            self.task.cancel()
        }
    }

}

extension DataDownloader {
    
    private func reset() {
        self.mutableData = Data()
        self.lastDate = Date()
        self.session?.invalidateAndCancel()
        self.offset = self.files.fileCurrentBytes()
        if self.offset > 0 {
            if let data = self.files.readData() {
                self.mutableData.append(data)
                let requestRange = String(format: "bytes=%llu-", self.offset)
                self.request.addValue(requestRange, forHTTPHeaderField: "Range")
            } else {
                self.offset = 0
                try? self.files.removeFileItem()
            }
        }
    }
    
    private func result(data: Data?, response: URLResponse?, state: Disposition) {
        switch state {
        case .downloading, .complete:
            if let data = data, data.isEmpty == false {
                self.completionHandler(state, data, response)
            } else {
                self.completionHandler(.failed(invalidDataError()), data, response)
            }
        case .finished:
            self.completionHandler(state, data, response)
        case .failed:
            self.retry.retry(task: task) { [weak self] state_ in
                switch state_ {
                case .retring:
                    self?.setupDataTask()
                case .stop:
                    self?.completionHandler(state, data, response)
                }
            }
        }
    }
    
    private func didReceiveData(data: Data, dataTask: URLSessionDataTask) {
        self.mutableData.append(data)
        if canDownloading() {
            let receiveBytes = dataTask.countOfBytesReceived + offset
            let allBytes = dataTask.countOfBytesExpectedToReceive + offset
            let currentProgress = min(max(0, CGFloat(receiveBytes) / CGFloat(allBytes)), 1)
            result(data: mutableData, response: dataTask.response, state: .downloading(currentProgress))
        }
    }
    
    private func canDownloading() -> Bool {
        let currentDate = Date()
        let time = currentDate.timeIntervalSince(lastDate)
        if time >= self.interval {
            lastDate = currentDate
            return true
        }
        return false
    }
    
    private func hasSuccessCode(_ response: HTTPURLResponse) -> Bool {
        switch response.statusCode {
        case 200 ..< 300:
            return true
        default:
            return false
        }
    }
    
    static let domain = "com.condy.ImageX.downloading"
    private func statusCodeError(_ statusCode: Int) -> NSError {
        let userInfo = [
            NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)
        ]
        return NSError(domain: DataDownloader.domain, code: statusCode, userInfo: userInfo)
    }
    
    private func invalidHTTPURLResponseError() -> NSError {
        let userInfo = [
            NSLocalizedDescriptionKey: "Did receive response is not HTTPURLResponse."
        ]
        return NSError(domain: DataDownloader.domain, code: 2002, userInfo: userInfo)
    }
    
    private func invalidDataError() -> NSError {
        let userInfo = [
            NSLocalizedDescriptionKey: "The downloaded data is empty."
        ]
        return NSError(domain: DataDownloader.domain, code: 3003, userInfo: userInfo)
    }
}

extension DataDownloader: URLSessionDataDelegate {
    
    public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
        guard let response = dataTask.response as? HTTPURLResponse else {
            result(data: nil, response: response, state: .failed(invalidHTTPURLResponseError()))
            completionHandler(.cancel)
            return
        }
        guard hasSuccessCode(response) else {
            result(data: nil, response: response, state: .failed(statusCodeError(response.statusCode)))
            completionHandler(.cancel)
            return
        }
        self.outputStream = OutputStream(url: URL(fileURLWithPath: files.path), append: true)
        self.outputStream?.open()
        if offset == 0 {
            var totalBytes = response.expectedContentLength
            let data = Data(bytes: &totalBytes, count: MemoryLayout.size(ofValue: totalBytes))
            do {
                try URL(fileURLWithPath: files.path).mt.setExtendedAttribute(data: data, forName: Files.totalBytesKey)
            } catch {
                result(data: nil, response: response, state: .failed(error))
                completionHandler(.cancel)
                return
            }
        }
        completionHandler(.allow)
    }
    
    public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        self.didReceiveData(data: data, dataTask: dataTask)
        self.outputStream?.write(Array(data), maxLength: data.count)
    }
    
    public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        self.session?.invalidateAndCancel()
        self.outputStream?.close()
        guard let response = task.response as? HTTPURLResponse else {
            result(data: nil, response: task.response, state: .failed(invalidHTTPURLResponseError()))
            return
        }
        if let error = error {
            let state: Disposition = self.retry.exceededRetriedCount() ? .finished(error) : .failed(error)
            result(data: nil, response: response, state: state)
        } else if hasSuccessCode(response) {
            result(data: mutableData, response: response, state: .complete)
            try? files.removeFileItem()
        } else {
            let error = statusCodeError(response.statusCode)
            let state: Disposition = self.retry.exceededRetriedCount() ? .finished(error) : .failed(error)
            result(data: nil, response: response, state: state)
        }
    }
}
final class DataDownloader: NSObject { enum Disposition { case downloading(CGFloat) case complete case failed(Error) case finished(Error) } private(set) var task: URLSessionDataTask! private(set) var session: URLSession? private(set) var retry: DelayRetry private(set) var request: URLRequest private(set) var outputStream: OutputStream? private(set) var lastDate: Date! /// Downloaded raw data of current task. private(set) var mutableData: Data! /// The downloaded part. private(set) var offset: Int64 = 0 /// Write to the resource file object. private(set) var files: Files! /// Network resource data download progress response interval. private(set) var interval: TimeInterval typealias DownloadBlock = ((_ state: Disposition, _ data: Data?, _ response: URLResponse?) -> Void) let completionHandler: DownloadBlock init(request: URLRequest, named: String, retry: DelayRetry, interval: TimeInterval, completionHandler: @escaping DownloadBlock) { self.retry = retry self.completionHandler = completionHandler self.request = request self.interval = interval super.init() do { self.files = try Files.init(named: named) } catch { self.result(data: nil, response: nil, state: .finished(error)) return } self.setupDataTask() } deinit { session?.invalidateAndCancel() } func setupDataTask() { self.reset() self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) self.task = session?.dataTask(with: request) self.task.resume() self.retry.increaseRetryCount() } func cancelTask() { self.session?.invalidateAndCancel() if self.task.state != .canceling { self.task.cancel() } } } extension DataDownloader { private func reset() { self.mutableData = Data() self.lastDate = Date() self.session?.invalidateAndCancel() self.offset = self.files.fileCurrentBytes() if self.offset > 0 { if let data = self.files.readData() { self.mutableData.append(data) let requestRange = String(format: "bytes=%llu-", self.offset) self.request.addValue(requestRange, forHTTPHeaderField: "Range") } else { self.offset = 0 try? self.files.removeFileItem() } } } private func result(data: Data?, response: URLResponse?, state: Disposition) { switch state { case .downloading, .complete: if let data = data, data.isEmpty == false { self.completionHandler(state, data, response) } else { self.completionHandler(.failed(invalidDataError()), data, response) } case .finished: self.completionHandler(state, data, response) case .failed: self.retry.retry(task: task) { [weak self] state_ in switch state_ { case .retring: self?.setupDataTask() case .stop: self?.completionHandler(state, data, response) } } } } private func didReceiveData(data: Data, dataTask: URLSessionDataTask) { self.mutableData.append(data) if canDownloading() { let receiveBytes = dataTask.countOfBytesReceived + offset let allBytes = dataTask.countOfBytesExpectedToReceive + offset let currentProgress = min(max(0, CGFloat(receiveBytes) / CGFloat(allBytes)), 1) result(data: mutableData, response: dataTask.response, state: .downloading(currentProgress)) } } private func canDownloading() -> Bool { let currentDate = Date() let time = currentDate.timeIntervalSince(lastDate) if time >= self.interval { lastDate = currentDate return true } return false } private func hasSuccessCode(_ response: HTTPURLResponse) -> Bool { switch response.statusCode { case 200 ..< 300: return true default: return false } } static let domain = "com.condy.ImageX.downloading" private func statusCodeError(_ statusCode: Int) -> NSError { let userInfo = [ NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode) ] return NSError(domain: DataDownloader.domain, code: statusCode, userInfo: userInfo) } private func invalidHTTPURLResponseError() -> NSError { let userInfo = [ NSLocalizedDescriptionKey: "Did receive response is not HTTPURLResponse." ] return NSError(domain: DataDownloader.domain, code: 2002, userInfo: userInfo) } private func invalidDataError() -> NSError { let userInfo = [ NSLocalizedDescriptionKey: "The downloaded data is empty." ] return NSError(domain: DataDownloader.domain, code: 3003, userInfo: userInfo) } } extension DataDownloader: URLSessionDataDelegate { public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { guard let response = dataTask.response as? HTTPURLResponse else { result(data: nil, response: response, state: .failed(invalidHTTPURLResponseError())) completionHandler(.cancel) return } guard hasSuccessCode(response) else { result(data: nil, response: response, state: .failed(statusCodeError(response.statusCode))) completionHandler(.cancel) return } self.outputStream = OutputStream(url: URL(fileURLWithPath: files.path), append: true) self.outputStream?.open() if offset == 0 { var totalBytes = response.expectedContentLength let data = Data(bytes: &totalBytes, count: MemoryLayout.size(ofValue: totalBytes)) do { try URL(fileURLWithPath: files.path).mt.setExtendedAttribute(data: data, forName: Files.totalBytesKey) } catch { result(data: nil, response: response, state: .failed(error)) completionHandler(.cancel) return } } completionHandler(.allow) } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { self.didReceiveData(data: data, dataTask: dataTask) self.outputStream?.write(Array(data), maxLength: data.count) } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { self.session?.invalidateAndCancel() self.outputStream?.close() guard let response = task.response as? HTTPURLResponse else { result(data: nil, response: task.response, state: .failed(invalidHTTPURLResponseError())) return } if let error = error { let state: Disposition = self.retry.exceededRetriedCount() ? .finished(error) : .failed(error) result(data: nil, response: response, state: state) } else if hasSuccessCode(response) { result(data: mutableData, response: response, state: .complete) try? files.removeFileItem() } else { let error = statusCodeError(response.statusCode) let state: Disposition = self.retry.exceededRetriedCount() ? .finished(error) : .failed(error) result(data: nil, response: response, state: state) } } }
  • 当然这边也对于网络下载失败,做了下载重试 DelayRetry 操作;

如何使用

  • 使用流程基本可以参考猫神所著Kingfisher,同样该库也采用这种模式,这样也方便大家使用习惯;
  • 这边鉴于后续参数的增加,因此采用 AnimatedOptions 来传递其余参数,方便扩展和操作;

基本使用

let url = URL(string: "https://example.com/image.png")!
imageView.mt.setImage(with: url)
let url = URL(string: "https://example.com/image.png")!
imageView.mt.setImage(with: url)
let url = URL(string: "https://example.com/image.png")! imageView.mt.setImage(with: url)

设置不同参数使用

var options = AnimatedOptions(moduleName: "Component Name") // 组件化需模块名
options.loop = .count(3) // 循环播放3次
options.placeholder = .image(R.image("IMG_0020")!) // 占位图
options.contentMode = .scaleAspectBottomRight // 填充模式
options.bufferCount = 20 // 缓存20帧
options.cacheOption = .disk // 采用磁盘缓存
options.cacheCrypto = .sha1 // 加密
options.cacheDataZip = .gzip // 压缩数据
options.retry = .max3s // 网络失败重试
options.setPreparationBlock(block: { [weak self] _ in
// do something..
})
options.setAnimatedBlock(block: { _ in
// play is complete and then do something..
})
options.setNetworkProgress(block: { _ in
// download progress..
})
options.setNetworkFailed(block: { _ in
// download failed.
})
let links = [``GIF URL``, ``Image URL``, ``GIF Named``, ``Image Named``]
let named = links.randomElement() ?? ""
// Setup filters.
let filters: [C7FilterProtocol] = [
C7SoulOut(soul: 0.75),
C7Storyboard(ranks: 2),
]
imageView.mt.setImage(with: named, filters: filters, options: options)
var options = AnimatedOptions(moduleName: "Component Name") // 组件化需模块名
options.loop = .count(3) // 循环播放3次
options.placeholder = .image(R.image("IMG_0020")!) // 占位图
options.contentMode = .scaleAspectBottomRight // 填充模式
options.bufferCount = 20 // 缓存20帧
options.cacheOption = .disk // 采用磁盘缓存
options.cacheCrypto = .sha1 // 加密
options.cacheDataZip = .gzip // 压缩数据
options.retry = .max3s // 网络失败重试
options.setPreparationBlock(block: { [weak self] _ in
    // do something..
})
options.setAnimatedBlock(block: { _ in
    // play is complete and then do something..
})
options.setNetworkProgress(block: { _ in
    // download progress..
})
options.setNetworkFailed(block: { _ in
    // download failed.
})

let links = [``GIF URL``, ``Image URL``, ``GIF Named``, ``Image Named``]
let named = links.randomElement() ?? ""
// Setup filters.
let filters: [C7FilterProtocol] = [
    C7SoulOut(soul: 0.75),
    C7Storyboard(ranks: 2),
]
imageView.mt.setImage(with: named, filters: filters, options: options)
var options = AnimatedOptions(moduleName: "Component Name") // 组件化需模块名 options.loop = .count(3) // 循环播放3次 options.placeholder = .image(R.image("IMG_0020")!) // 占位图 options.contentMode = .scaleAspectBottomRight // 填充模式 options.bufferCount = 20 // 缓存20帧 options.cacheOption = .disk // 采用磁盘缓存 options.cacheCrypto = .sha1 // 加密 options.cacheDataZip = .gzip // 压缩数据 options.retry = .max3s // 网络失败重试 options.setPreparationBlock(block: { [weak self] _ in // do something.. }) options.setAnimatedBlock(block: { _ in // play is complete and then do something.. }) options.setNetworkProgress(block: { _ in // download progress.. }) options.setNetworkFailed(block: { _ in // download failed. }) let links = [``GIF URL``, ``Image URL``, ``GIF Named``, ``Image Named``] let named = links.randomElement() ?? "" // Setup filters. let filters: [C7FilterProtocol] = [ C7SoulOut(soul: 0.75), C7Storyboard(ranks: 2), ] imageView.mt.setImage(with: named, filters: filters, options: options)
  • 快速让控件播放GIF和添加滤镜
class AnimatedView: UIView, AsAnimatable {
...
}
class AnimatedView: UIView, AsAnimatable {
    ...
}
class AnimatedView: UIView, AsAnimatable {     ... }
let filters: [C7FilterProtocol] = [
C7WhiteBalance(temperature: 5555),
C7Storyboard(ranks: 3)
]
let data = R.gifData("pikachu")
var options = AnimatedOptions()
options.loop = .forever
options.placeholder = .view(placeholder)
animatedView.play(data: data, filters: filters, options: options)
let filters: [C7FilterProtocol] = [
    C7WhiteBalance(temperature: 5555),
    C7Storyboard(ranks: 3)
]
let data = R.gifData("pikachu")
var options = AnimatedOptions()
options.loop = .forever
options.placeholder = .view(placeholder)
animatedView.play(data: data, filters: filters, options: options)
let filters: [C7FilterProtocol] = [ C7WhiteBalance(temperature: 5555), C7Storyboard(ranks: 3) ] let data = R.gifData("pikachu") var options = AnimatedOptions() options.loop = .forever options.placeholder = .view(placeholder) animatedView.play(data: data, filters: filters, options: options)
  • AnimatedOptions参数介绍
public struct AnimatedOptions {
public static var `default` = AnimatedOptions()
/// Desired number of loops. Default is ``forever``.
public var loop: ImageX.Loop = .forever
/// 如果遇见设置`original`以外其他模式显示无效`铺满屏幕`的情况,
/// 请将承载控件``view.contentMode = .scaleAspectFit``
/// Content mode used for resizing the frames. Default is ``original``.
public var contentMode: ImageX.ContentMode = .original
/// The number of frames to buffer. Default is 50. A high number will result in more memory usage and less CPU load, and vice versa.
public var bufferCount: Int = 50
/// Weather or not we should cache the URL response. Default is ``diskAndMemory``.
public var cacheOption: Lemons.CachedOptions = .diskAndMemory
/// Placeholder image. default gray picture.
public var placeholder: ImageX.Placeholder = .none
/// Network data cache naming encryption method, Default is ``md5``.
public var cacheCrypto: Lemons.CryptoType = .md5
/// Network data compression or decompression method, default ``gzip``.
/// This operation is done in the subthread.
public var cacheDataZip: ImageX.ZipType = .gzip
/// Network max retry count and retry interval, default max retry count is ``3`` and retry ``3s`` interval mechanism.
public var retry: ImageX.DelayRetry = .max3s
/// Confirm the size to facilitate follow-up processing, Default display control size.
public var confirmSize: CGSize = .zero
/// Web images or GIFs link download priority.
public var downloadPriority: Float = URLSessionTask.defaultPriority
/// The timeout interval for the request. Defaults to 20.0
public var timeoutInterval: TimeInterval = 20
/// Network resource data download progress response interval.
public var downloadInterval: TimeInterval = 0.02
/// 做组件化操作时刻,解决本地GIF或本地图片所处于另外模块从而读不出数据问题。?
/// Do the component operation to solve the problem that the local GIF or Image cannot read the data in another module.
public let moduleName: String
/// Instantiation of GIF configuration parameters.
/// - Parameters:
/// - moduleName: Do the component operation to solve the problem that the local GIF cannot read the data in another module.
public init(moduleName: String = "ImageX") {
self.moduleName = moduleName
}
internal var preparation: ((_ res: ImageX.GIFResponse) -> Void)?
/// Ready to play time callback.
/// - Parameter block: Prepare to play the callback.
public mutating func setPreparationBlock(block: @escaping ((_ res: ImageX.GIFResponse) -> Void)) {
self.preparation = block
}
internal var animated: ((_ loopDuration: TimeInterval) -> Void)?
/// GIF animation playback completed.
/// - Parameter block: Complete the callback.
public mutating func setAnimatedBlock(block: @escaping ((_ loopDuration: TimeInterval) -> Void)) {
self.animated = block
}
internal var failed: ((_ error: Error) -> Void)?
/// Network download task failure information.
/// - Parameter block: Failed the callback.
public mutating func setNetworkFailed(block: @escaping ((_ error: Error) -> Void)) {
self.failed = block
}
internal var progressBlock: ((_ currentProgress: CGFloat) -> Void)?
/// Network data task download progress.
/// - Parameter block: Download the callback.
public mutating func setNetworkProgress(block: @escaping ((_ currentProgress: CGFloat) -> Void)) {
self.progressBlock = block
}
internal var displayed: Bool = false // 防止重复设置占位信息
internal func setDisplayed(placeholder displayed: Bool) -> Self {
var options = self
options.displayed = displayed
return options
}
}
public struct AnimatedOptions {
    



    public static var `default` = AnimatedOptions()
    
    /// Desired number of loops. Default is ``forever``.
    public var loop: ImageX.Loop = .forever
    

    /// 如果遇见设置`original`以外其他模式显示无效`铺满屏幕`的情况,
    /// 请将承载控件``view.contentMode = .scaleAspectFit``
    /// Content mode used for resizing the frames. Default is ``original``.
    public var contentMode: ImageX.ContentMode = .original
    
    /// The number of frames to buffer. Default is 50. A high number will result in more memory usage and less CPU load, and vice versa.
    public var bufferCount: Int = 50
    
    /// Weather or not we should cache the URL response. Default is ``diskAndMemory``.
    public var cacheOption: Lemons.CachedOptions = .diskAndMemory
    
    /// Placeholder image. default gray picture.
    public var placeholder: ImageX.Placeholder = .none
    
    /// Network data cache naming encryption method, Default is ``md5``.
    public var cacheCrypto: Lemons.CryptoType = .md5
    

    /// Network data compression or decompression method, default ``gzip``.
    /// This operation is done in the subthread.
    public var cacheDataZip: ImageX.ZipType = .gzip
    
    /// Network max retry count and retry interval, default max retry count is ``3`` and retry ``3s`` interval mechanism.
    public var retry: ImageX.DelayRetry = .max3s
    
    /// Confirm the size to facilitate follow-up processing, Default display control size.
    public var confirmSize: CGSize = .zero
    
    /// Web images or GIFs link download priority.
    public var downloadPriority: Float = URLSessionTask.defaultPriority
    
    /// The timeout interval for the request. Defaults to 20.0
    public var timeoutInterval: TimeInterval = 20
    
    /// Network resource data download progress response interval.
    public var downloadInterval: TimeInterval = 0.02
    
    /// 做组件化操作时刻,解决本地GIF或本地图片所处于另外模块从而读不出数据问题。?
    /// Do the component operation to solve the problem that the local GIF or Image cannot read the data in another module.
    public let moduleName: String
    
    /// Instantiation of GIF configuration parameters.
    /// - Parameters:
    ///   - moduleName: Do the component operation to solve the problem that the local GIF cannot read the data in another module.
    public init(moduleName: String = "ImageX") {
        self.moduleName = moduleName
    }

    

    internal var preparation: ((_ res: ImageX.GIFResponse) -> Void)?
    /// Ready to play time callback.
    /// - Parameter block: Prepare to play the callback.
    public mutating func setPreparationBlock(block: @escaping ((_ res: ImageX.GIFResponse) -> Void)) {
        self.preparation = block
    }

    
    internal var animated: ((_ loopDuration: TimeInterval) -> Void)?
    /// GIF animation playback completed.
    /// - Parameter block: Complete the callback.
    public mutating func setAnimatedBlock(block: @escaping ((_ loopDuration: TimeInterval) -> Void)) {
        self.animated = block
    }

    
    internal var failed: ((_ error: Error) -> Void)?
    /// Network download task failure information.
    /// - Parameter block: Failed the callback.
    public mutating func setNetworkFailed(block: @escaping ((_ error: Error) -> Void)) {
        self.failed = block
    }
    
    internal var progressBlock: ((_ currentProgress: CGFloat) -> Void)?
    /// Network data task download progress.
    /// - Parameter block: Download the callback.
    public mutating func setNetworkProgress(block: @escaping ((_ currentProgress: CGFloat) -> Void)) {
        self.progressBlock = block
    }
    
    internal var displayed: Bool = false // 防止重复设置占位信息
    internal func setDisplayed(placeholder displayed: Bool) -> Self {
        var options = self
        options.displayed = displayed
        return options
    }
}
public struct AnimatedOptions { public static var `default` = AnimatedOptions() /// Desired number of loops. Default is ``forever``. public var loop: ImageX.Loop = .forever /// 如果遇见设置`original`以外其他模式显示无效`铺满屏幕`的情况, /// 请将承载控件``view.contentMode = .scaleAspectFit`` /// Content mode used for resizing the frames. Default is ``original``. public var contentMode: ImageX.ContentMode = .original /// The number of frames to buffer. Default is 50. A high number will result in more memory usage and less CPU load, and vice versa. public var bufferCount: Int = 50 /// Weather or not we should cache the URL response. Default is ``diskAndMemory``. public var cacheOption: Lemons.CachedOptions = .diskAndMemory /// Placeholder image. default gray picture. public var placeholder: ImageX.Placeholder = .none /// Network data cache naming encryption method, Default is ``md5``. public var cacheCrypto: Lemons.CryptoType = .md5 /// Network data compression or decompression method, default ``gzip``. /// This operation is done in the subthread. public var cacheDataZip: ImageX.ZipType = .gzip /// Network max retry count and retry interval, default max retry count is ``3`` and retry ``3s`` interval mechanism. public var retry: ImageX.DelayRetry = .max3s /// Confirm the size to facilitate follow-up processing, Default display control size. public var confirmSize: CGSize = .zero /// Web images or GIFs link download priority. public var downloadPriority: Float = URLSessionTask.defaultPriority /// The timeout interval for the request. Defaults to 20.0 public var timeoutInterval: TimeInterval = 20 /// Network resource data download progress response interval. public var downloadInterval: TimeInterval = 0.02 /// 做组件化操作时刻,解决本地GIF或本地图片所处于另外模块从而读不出数据问题。? /// Do the component operation to solve the problem that the local GIF or Image cannot read the data in another module. public let moduleName: String /// Instantiation of GIF configuration parameters. /// - Parameters: /// - moduleName: Do the component operation to solve the problem that the local GIF cannot read the data in another module. public init(moduleName: String = "ImageX") { self.moduleName = moduleName } internal var preparation: ((_ res: ImageX.GIFResponse) -> Void)? /// Ready to play time callback. /// - Parameter block: Prepare to play the callback. public mutating func setPreparationBlock(block: @escaping ((_ res: ImageX.GIFResponse) -> Void)) { self.preparation = block } internal var animated: ((_ loopDuration: TimeInterval) -> Void)? /// GIF animation playback completed. /// - Parameter block: Complete the callback. public mutating func setAnimatedBlock(block: @escaping ((_ loopDuration: TimeInterval) -> Void)) { self.animated = block } internal var failed: ((_ error: Error) -> Void)? /// Network download task failure information. /// - Parameter block: Failed the callback. public mutating func setNetworkFailed(block: @escaping ((_ error: Error) -> Void)) { self.failed = block } internal var progressBlock: ((_ currentProgress: CGFloat) -> Void)? /// Network data task download progress. /// - Parameter block: Download the callback. public mutating func setNetworkProgress(block: @escaping ((_ currentProgress: CGFloat) -> Void)) { self.progressBlock = block } internal var displayed: Bool = false // 防止重复设置占位信息 internal func setDisplayed(placeholder displayed: Bool) -> Self { var options = self options.displayed = displayed return options } }

总结

本文只是对网络图像和GIF显示的轻量化解决方案,让网图显示更加便捷,方便开发和后续迭代修改。实现方案还有许多可以改进的地方;
欢迎大家来使用该框架,然后指正修改亦或者大家有什么需求也可提出来,后续慢慢补充完善;
也欢迎大神来帮忙使用优化此库,再次感谢!!!

本库使用的滤镜库 Harbeth 和磁盘缓存库 Lemons 也欢迎大家使用;


对于如何使用和设计原理先简单介绍出来,关于后续功能和优化再慢慢介绍!

觉得有帮助的铁子,就给我点个星?支持一哈,谢谢铁子们~
本文图像滤镜框架传送门 ImageX 地址。
有什么问题也可以直接联系我,邮箱 yangkj310@gmail.com

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYUHJvda' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片