2 // ImageView+Kingfisher.swift
5 // Created by Wei Wang on 15/4/6.
7 // Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34 // MARK: - Extension methods.
36 * Set image to use from web.
38 extension Kingfisher where Base: ImageView {
40 Set an image with a resource, a placeholder image, options, progress handler and completion handler.
42 - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`.
43 - parameter placeholder: A placeholder image when retrieving the image at URL.
44 - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
45 - parameter progressBlock: Called when the image downloading progress gets updated.
46 - parameter completionHandler: Called when the image retrieved and set.
48 - returns: A task represents the retrieving process.
50 - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread.
51 The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method.
53 If `resource` is `nil`, the `placeholder` image will be set and
54 `completionHandler` will be called with both `error` and `image` being `nil`.
57 public func setImage(with resource: Resource?,
58 placeholder: Placeholder? = nil,
59 options: KingfisherOptionsInfo? = nil,
60 progressBlock: DownloadProgressBlock? = nil,
61 completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
63 guard let resource = resource else {
64 self.placeholder = placeholder
66 completionHandler?(nil, nil, .none, nil)
70 var options = KingfisherManager.shared.defaultOptions + (options ?? KingfisherEmptyOptionsInfo)
71 let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
73 if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet { // Always set placeholder while there is no image/placehoer yet.
74 self.placeholder = placeholder
77 let maybeIndicator = indicator
78 maybeIndicator?.startAnimatingView()
80 setWebURL(resource.downloadURL)
82 if base.shouldPreloadAllAnimation() {
83 options.append(.preloadAllAnimationData)
86 let task = KingfisherManager.shared.retrieveImage(
89 progressBlock: { receivedSize, totalSize in
90 guard resource.downloadURL == self.webURL else {
93 if let progressBlock = progressBlock {
94 progressBlock(receivedSize, totalSize)
97 completionHandler: {[weak base] image, error, cacheType, imageURL in
98 DispatchQueue.main.safeAsync {
99 maybeIndicator?.stopAnimatingView()
100 guard let strongBase = base, imageURL == self.webURL else {
101 completionHandler?(image, error, cacheType, imageURL)
105 self.setImageTask(nil)
106 guard let image = image else {
107 completionHandler?(nil, error, cacheType, imageURL)
111 guard let transitionItem = options.lastMatchIgnoringAssociatedValue(.transition(.none)),
112 case .transition(let transition) = transitionItem, ( options.forceTransition || cacheType == .none) else
114 self.placeholder = nil
115 strongBase.image = image
116 completionHandler?(image, error, cacheType, imageURL)
121 UIView.transition(with: strongBase, duration: 0.0, options: [],
122 animations: { maybeIndicator?.stopAnimatingView() },
125 self.placeholder = nil
126 UIView.transition(with: strongBase, duration: transition.duration,
127 options: [transition.animationOptions, .allowUserInteraction],
129 // Set image property in the animation.
130 transition.animations?(strongBase, image)
132 completion: { finished in
133 transition.completion?(finished)
134 completionHandler?(image, error, cacheType, imageURL)
147 Cancel the image download task bounded to the image view if it is running.
148 Nothing will happen if the downloading has already finished.
150 public func cancelDownloadTask() {
155 // MARK: - Associated Object
156 private var lastURLKey: Void?
157 private var indicatorKey: Void?
158 private var indicatorTypeKey: Void?
159 private var placeholderKey: Void?
160 private var imageTaskKey: Void?
162 extension Kingfisher where Base: ImageView {
163 /// Get the image URL binded to this image view.
164 public var webURL: URL? {
165 return objc_getAssociatedObject(base, &lastURLKey) as? URL
168 fileprivate func setWebURL(_ url: URL?) {
169 objc_setAssociatedObject(base, &lastURLKey, url, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
172 /// Holds which indicator type is going to be used.
173 /// Default is .none, means no indicator will be shown.
174 public var indicatorType: IndicatorType {
176 let indicator = objc_getAssociatedObject(base, &indicatorTypeKey) as? IndicatorType
177 return indicator ?? .none
185 indicator = ActivityIndicator()
186 case .image(let data):
187 indicator = ImageIndicator(imageData: data)
188 case .custom(let anIndicator):
189 indicator = anIndicator
192 objc_setAssociatedObject(base, &indicatorTypeKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
196 /// Holds any type that conforms to the protocol `Indicator`.
197 /// The protocol `Indicator` has a `view` property that will be shown when loading an image.
198 /// It will be `nil` if `indicatorType` is `.none`.
199 public fileprivate(set) var indicator: Indicator? {
201 guard let box = objc_getAssociatedObject(base, &indicatorKey) as? Box<Indicator> else {
209 if let previousIndicator = indicator {
210 previousIndicator.view.removeFromSuperview()
214 if var newIndicator = newValue {
215 // Set default indicator frame if the view's frame not set.
216 if newIndicator.view.frame == .zero {
217 newIndicator.view.frame = base.frame
219 newIndicator.viewCenter = CGPoint(x: base.bounds.midX, y: base.bounds.midY)
220 newIndicator.view.isHidden = true
221 base.addSubview(newIndicator.view)
224 // Save in associated object
225 // Wrap newValue with Box to workaround an issue that Swift does not recognize
226 // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872
227 objc_setAssociatedObject(base, &indicatorKey, newValue.map(Box.init), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
231 fileprivate var imageTask: RetrieveImageTask? {
232 return objc_getAssociatedObject(base, &imageTaskKey) as? RetrieveImageTask
235 fileprivate func setImageTask(_ task: RetrieveImageTask?) {
236 objc_setAssociatedObject(base, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
239 public fileprivate(set) var placeholder: Placeholder? {
241 return objc_getAssociatedObject(base, &placeholderKey) as? Placeholder
245 if let previousPlaceholder = placeholder {
246 previousPlaceholder.remove(from: base)
249 if let newPlaceholder = newValue {
250 newPlaceholder.add(to: base)
255 objc_setAssociatedObject(base, &placeholderKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
261 @objc extension ImageView {
262 func shouldPreloadAllAnimation() -> Bool { return true }