一、背景
日常开发过程中,经常需要在各类型控件上加载显示图像orGIF,例如:UIImageView、UIBtton、NSImageView、NSButton等等。
这时候有个网络图像库就会便利太多太多,很多人这时候会说,对于这块其实网络上有很多,比如 Kingfisher、 YYWebImage等等。
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!
实现方案
这边主要就是分为以下几大模块,网络下载模块、资源缓存模块、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:breakcase .image:container_.image = imagecase .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:breakcase .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:breakcase .image:container_.image = imagecase .highlightedImage:container_.highlightedImage = image}#endif#if canImport(WatchKit)case var container_ as WKInterfaceImageContainer:container_.image = imagecontainer_.setImage(image)#endifdefault:#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:
image
和highlightedImage
- NSImageVIew:
image
- UIButton:
image
和backgroundImage
- NSButton:
image
和alternateImage
- WKInterfaceImage:
image
对于UIView没有上述属性显示,so 这边对layer.contents
设置也是同样能达到该效果。
如何下载网络资源
对于网络图像显示,不可获取的就是对于资源的下载。
最开始的简单版,
let task = URLSession.shared.dataTask(with: url) { (data, _, error) inswitch (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 = truerequest.cachePolicy = .reloadIgnoringLocalCacheDataif #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] = downloaderreturn 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 completecase failed(Error)case finished(Error)}private(set) var task: URLSessionDataTask!private(set) var session: URLSession?private(set) var retry: DelayRetryprivate(set) var request: URLRequestprivate(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: TimeIntervaltypealias DownloadBlock = ((_ state: Disposition, _ data: Data?, _ response: URLResponse?) -> Void)let completionHandler: DownloadBlockinit(request: URLRequest, named: String, retry: DelayRetry, interval: TimeInterval, completionHandler: @escaping DownloadBlock) {self.retry = retryself.completionHandler = completionHandlerself.request = requestself.interval = intervalsuper.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 = 0try? 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_ inswitch 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 + offsetlet allBytes = dataTask.countOfBytesExpectedToReceive + offsetlet 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 = currentDatereturn true}return false}private func hasSuccessCode(_ response: HTTPURLResponse) -> Bool {switch response.statusCode {case 200 ..< 300:return truedefault: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.expectedContentLengthlet 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 = .foreveroptions.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.0public 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 = selfoptions.displayed = displayedreturn 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