Răsfoiți Sursa

Merge pull request #430 from onevcat/jdmoreira-master

Jdmoreira master
Wei Wang 9 ani în urmă
părinte
comite
bdf4c90229

+ 2 - 4
Demo/Kingfisher-Demo/ViewController.swift

@@ -64,12 +64,10 @@ extension ViewController {
     override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
         
         let cell = collectionView.dequeueReusableCellWithReuseIdentifier("collectionViewCell", forIndexPath: indexPath) as! CollectionViewCell
-        
-        cell.cellImageView.kf_showIndicatorWhenLoading = true
-        
+                
         let URL = NSURL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!
         
-        
+        cell.cellImageView.kf_showIndicatorWhenLoading = true
         cell.cellImageView.kf_setImageWithURL(URL, placeholderImage: nil,
                                                         optionsInfo: [.Transition(ImageTransition.Fade(1))],
                                                       progressBlock: { receivedSize, totalSize in

+ 6 - 1
Demo/Kingfisher-OSX-Demo/ViewController.swift

@@ -61,9 +61,14 @@ extension ViewController: NSCollectionViewDataSource {
     func collectionView(collectionView: NSCollectionView, itemForRepresentedObjectAtIndexPath indexPath: NSIndexPath) -> NSCollectionViewItem {
         let item = collectionView.makeItemWithIdentifier("Cell", forIndexPath: indexPath)
         
+        if let loaderPath = NSBundle.mainBundle().pathForResource("loader", ofType: "gif") {
+            if let loaderData = NSData(contentsOfFile: loaderPath) {
+                item.imageView?.kf_indicatorType = .image(imageData: loaderData)
+            }
+        }
+        
         let URL = NSURL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.item + 1).jpg")!
         
-        item.imageView?.kf_showIndicatorWhenLoading = true
         item.imageView?.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil,
                                                    progressBlock: { receivedSize, totalSize in
                                                     print("\(indexPath.item + 1): \(receivedSize)/\(totalSize)")

+ 26 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -19,6 +19,15 @@
 		4B98674F1CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B98674E1CD1CF42003ADAC7 /* AnimatedImageView.swift */; };
 		4B9867501CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B98674E1CD1CF42003ADAC7 /* AnimatedImageView.swift */; };
 		B43007AC86DBFFFD1AC6EDD1 /* libPods-KingfisherTests-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 798E024A9311DC80470CF240 /* libPods-KingfisherTests-tvOS.a */; };
+		CD2C73541D7F040B00A1D819 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2C73531D7F040B00A1D819 /* Indicator.swift */; };
+		CD2C73551D7F040B00A1D819 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2C73531D7F040B00A1D819 /* Indicator.swift */; };
+		CD2C73561D7F040B00A1D819 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2C73531D7F040B00A1D819 /* Indicator.swift */; };
+		CD2C736B1D80231E00A1D819 /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = CD2C736A1D80231E00A1D819 /* loader.gif */; };
+		CD2C736D1D80234F00A1D819 /* loader.gif in Resources */ = {isa = PBXBuildFile; fileRef = CD2C736C1D80234F00A1D819 /* loader.gif */; };
+		CD4593971D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */; };
+		CD4593981D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */; };
+		CD4593991D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */; };
+		CD45939A1D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */; };
 		D10945F71C526B86001408EB /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10945EA1C526B6C001408EB /* Image.swift */; };
 		D10945F81C526B86001408EB /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10945EB1C526B6C001408EB /* ImageCache.swift */; };
 		D10945F91C526B86001408EB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D10945EC1C526B6C001408EB /* ImageDownloader.swift */; };
@@ -267,6 +276,10 @@
 		A8D69912DD16C2942EB1F40E /* Pods-KingfisherTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests/Pods-KingfisherTests.release.xcconfig"; sourceTree = "<group>"; };
 		A9E621E297FEFAD35D39C34E /* libPods-KingfisherTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-KingfisherTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		B6B5C590A36C8E84C5B16C3E /* Pods-KingfisherTests-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests-tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests-tvOS/Pods-KingfisherTests-tvOS.debug.xcconfig"; sourceTree = "<group>"; };
+		CD2C73531D7F040B00A1D819 /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Indicator.swift; sourceTree = "<group>"; };
+		CD2C736A1D80231E00A1D819 /* loader.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = loader.gif; path = images/loader.gif; sourceTree = SOURCE_ROOT; };
+		CD2C736C1D80234F00A1D819 /* loader.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; name = loader.gif; path = images/loader.gif; sourceTree = SOURCE_ROOT; };
+		CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WrappedAssociatedObjects.swift; path = Sources/WrappedAssociatedObjects.swift; sourceTree = "<group>"; };
 		D10945EA1C526B6C001408EB /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image.swift; sourceTree = "<group>"; };
 		D10945EB1C526B6C001408EB /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/ImageCache.swift; sourceTree = "<group>"; };
 		D10945EC1C526B6C001408EB /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/ImageDownloader.swift; sourceTree = "<group>"; };
@@ -459,6 +472,8 @@
 				D10945F51C526B6C001408EB /* ThreadHelper.swift */,
 				D10945F61C526B6C001408EB /* UIButton+Kingfisher.swift */,
 				182FFF771CC9ACBA004B728D /* NSButton+Kingfisher.swift */,
+				CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */,
+				CD2C73531D7F040B00A1D819 /* Indicator.swift */,
 			);
 			name = Sources;
 			sourceTree = "<group>";
@@ -528,6 +543,7 @@
 		D12E0C8B1C47F91800AC98AD /* Kingfisher-Demo */ = {
 			isa = PBXGroup;
 			children = (
+				CD2C736C1D80234F00A1D819 /* loader.gif */,
 				D12E0C8C1C47F91800AC98AD /* AppDelegate.swift */,
 				D12E0C8D1C47F91800AC98AD /* LaunchScreen.xib */,
 				D12E0C8F1C47F91800AC98AD /* Main.storyboard */,
@@ -555,6 +571,7 @@
 		D12E0CA61C47F92C00AC98AD /* Kingfisher-OSX-Demo */ = {
 			isa = PBXGroup;
 			children = (
+				CD2C736A1D80231E00A1D819 /* loader.gif */,
 				D12E0CA71C47F92C00AC98AD /* AppDelegate.swift */,
 				D12E0CA81C47F92C00AC98AD /* Assets.xcassets */,
 				D12E0CA91C47F92C00AC98AD /* Main.storyboard */,
@@ -991,6 +1008,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				D12E0CAF1C47F92C00AC98AD /* Assets.xcassets in Resources */,
+				CD2C736B1D80231E00A1D819 /* loader.gif in Resources */,
 				D12E0CB11C47F92C00AC98AD /* Cell.xib in Resources */,
 				D12E0CB01C47F92C00AC98AD /* Main.storyboard in Resources */,
 			);
@@ -1059,6 +1077,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				D12E0C991C47F91800AC98AD /* Images.xcassets in Resources */,
+				CD2C736D1D80234F00A1D819 /* loader.gif in Resources */,
 				D12E0C961C47F91800AC98AD /* LaunchScreen.xib in Resources */,
 				D12E0C971C47F91800AC98AD /* Main.storyboard in Resources */,
 			);
@@ -1226,12 +1245,14 @@
 			buildActionMask = 2147483647;
 			files = (
 				D109461A1C526C61001408EB /* Image.swift in Sources */,
+				CD2C73561D7F040B00A1D819 /* Indicator.swift in Sources */,
 				D109461B1C526C61001408EB /* ImageCache.swift in Sources */,
 				D109461C1C526C61001408EB /* ImageDownloader.swift in Sources */,
 				D109461D1C526C61001408EB /* ImageTransition.swift in Sources */,
 				D109461E1C526C61001408EB /* ImageView+Kingfisher.swift in Sources */,
 				D109461F1C526C61001408EB /* KingfisherManager.swift in Sources */,
 				182FFF781CC9ACBA004B728D /* NSButton+Kingfisher.swift in Sources */,
+				CD4593991D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */,
 				D10946201C526C61001408EB /* KingfisherOptionsInfo.swift in Sources */,
 				D10946211C526C61001408EB /* Resource.swift in Sources */,
 				D9638BA21C7DBA660046523D /* ImagePrefetcher.swift in Sources */,
@@ -1297,6 +1318,7 @@
 			files = (
 				D109460E1C526C0D001408EB /* Image.swift in Sources */,
 				4B9867501CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */,
+				CD4593981D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */,
 				D109460F1C526C0D001408EB /* ImageCache.swift in Sources */,
 				D10946101C526C0D001408EB /* ImageDownloader.swift in Sources */,
 				D10946111C526C0D001408EB /* ImageTransition.swift in Sources */,
@@ -1307,6 +1329,7 @@
 				D10946151C526C0D001408EB /* Resource.swift in Sources */,
 				D10946161C526C0D001408EB /* String+MD5.swift in Sources */,
 				D10946171C526C0D001408EB /* ThreadHelper.swift in Sources */,
+				CD2C73551D7F040B00A1D819 /* Indicator.swift in Sources */,
 				D10946181C526C0D001408EB /* UIButton+Kingfisher.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1317,6 +1340,7 @@
 			files = (
 				D109462D1C526CF5001408EB /* ImageTransition.swift in Sources */,
 				D10946251C526CE8001408EB /* Image.swift in Sources */,
+				CD45939A1D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */,
 				D10946261C526CE8001408EB /* ImageCache.swift in Sources */,
 				D9638BA31C7DBA660046523D /* ImagePrefetcher.swift in Sources */,
 				D10946271C526CE8001408EB /* ImageDownloader.swift in Sources */,
@@ -1353,6 +1377,7 @@
 			files = (
 				D10945F71C526B86001408EB /* Image.swift in Sources */,
 				4B98674F1CD1CF42003ADAC7 /* AnimatedImageView.swift in Sources */,
+				CD4593971D7EAB9900FD570E /* WrappedAssociatedObjects.swift in Sources */,
 				D10945F81C526B86001408EB /* ImageCache.swift in Sources */,
 				D10945F91C526B86001408EB /* ImageDownloader.swift in Sources */,
 				D10945FA1C526B86001408EB /* ImageTransition.swift in Sources */,
@@ -1363,6 +1388,7 @@
 				D10945FE1C526B86001408EB /* Resource.swift in Sources */,
 				D10945FF1C526B86001408EB /* String+MD5.swift in Sources */,
 				D10946001C526B86001408EB /* ThreadHelper.swift in Sources */,
+				CD2C73541D7F040B00A1D819 /* Indicator.swift in Sources */,
 				D10946011C526B86001408EB /* UIButton+Kingfisher.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 8 - 8
Sources/Image.swift

@@ -56,19 +56,19 @@ extension Image {
     
     private(set) var kf_images: [Image]? {
         get {
-            return objc_getAssociatedObject(self, &imagesKey) as? [Image]
+            return getAssociatedObject(self, associativeKey: &imagesKey)
         }
         set {
-            objc_setAssociatedObject(self, &imagesKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+            setAssociatedObject(self, value: newValue, associativeKey: &imagesKey)
         }
     }
     
     private(set) var kf_duration: NSTimeInterval {
         get {
-            return objc_getAssociatedObject(self, &durationKey) as? NSTimeInterval ?? 0.0
+            return getAssociatedObject(self, associativeKey: &durationKey) ?? 0.0
         }
         set {
-            objc_setAssociatedObject(self, &durationKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+            setAssociatedObject(self, value: newValue, associativeKey: &durationKey)
         }
     }
     
@@ -87,19 +87,19 @@ extension Image {
     
     private(set) var kf_imageSource: ImageSource? {
             get {
-                return objc_getAssociatedObject(self, &imageSourceKey) as? ImageSource
+                return getAssociatedObject(self, associativeKey: &imageSourceKey)
             }
             set {
-                objc_setAssociatedObject(self, &imageSourceKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+                setAssociatedObject(self, value: newValue, associativeKey: &imageSourceKey)
             }
         }
         
     private(set) var kf_animatedImageData: NSData? {
             get {
-                return objc_getAssociatedObject(self, &animatedImageDataKey) as? NSData
+                return getAssociatedObject(self, associativeKey: &animatedImageDataKey)
             }
             set {
-                objc_setAssociatedObject(self, &animatedImageDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+                setAssociatedObject(self, value: newValue, associativeKey: &animatedImageDataKey)
             }
         }
 #endif

+ 86 - 103
Sources/ImageView+Kingfisher.swift

@@ -28,11 +28,9 @@
 #if os(OSX)
 import AppKit
 typealias ImageView = NSImageView
-public typealias IndicatorView = NSProgressIndicator
 #else
 import UIKit
 typealias ImageView = UIImageView
-public typealias IndicatorView = UIActivityIndicatorView
 #endif
 
 // MARK: - Set Images
@@ -98,13 +96,8 @@ extension ImageView {
             return RetrieveImageTask.emptyTask
         }
         
-        let showIndicatorWhenLoading = kf_showIndicatorWhenLoading
-        var indicator: IndicatorView? = nil
-        if showIndicatorWhenLoading {
-            indicator = kf_indicator
-            indicator?.hidden = false
-            indicator?.kf_startAnimating()
-        }
+        let maybeIndicator = kf_indicator
+        maybeIndicator?.startAnimatingView()
         
         kf_setWebURL(resource.downloadURL)
         
@@ -129,7 +122,7 @@ extension ImageView {
                     sSelf.kf_setImageTask(nil)
                     
                     guard let image = image else {
-                        indicator?.kf_stopAnimating()
+                        maybeIndicator?.stopAnimatingView()
                         completionHandler?(image: nil, error: error, cacheType: cacheType, imageURL: imageURL)
                         return
                     }
@@ -139,7 +132,7 @@ extension ImageView {
                             #if !os(OSX)
                                 UIView.transitionWithView(sSelf, duration: 0.0, options: [],
                                     animations: {
-                                        indicator?.kf_stopAnimating()
+                                        maybeIndicator?.stopAnimatingView()
                                     },
                                     completion: { finished in
                                         UIView.transitionWithView(sSelf, duration: transition.duration,
@@ -155,7 +148,7 @@ extension ImageView {
                                 })
                             #endif
                     } else {
-                        indicator?.kf_stopAnimating()
+                        maybeIndicator?.stopAnimatingView()
                         sSelf.image = image
                         completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
                     }
@@ -184,30 +177,50 @@ extension ImageView {
     }
 }
 
+/**
+ Enum for the types of indicators that the user can choose from.
+ */
+extension ImageView {
+    public enum IndicatorType {
+        /// No indicator.
+        case None
+        /// Use system activity indicator.
+        case Activity
+        /// Use an image as indicator. GIF is supported.
+        case Image(imageData: NSData)
+        /// Use a custom indicator, which conforms to the `Indicator` protocol.
+        case Custom(indicator: Indicator)
+    }
+}
+
 // MARK: - Associated Object
 private var lastURLKey: Void?
 private var indicatorKey: Void?
 private var showIndicatorWhenLoadingKey: Void?
+private var indicatorTypeKey: Void?
 private var imageTaskKey: Void?
 
 extension ImageView {
     /// Get the image URL binded to this image view.
     public var kf_webURL: NSURL? {
-        return objc_getAssociatedObject(self, &lastURLKey) as? NSURL
+        return getAssociatedObject(self, associativeKey: &lastURLKey)
     }
     
     private func kf_setWebURL(URL: NSURL) {
-        objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: URL, associativeKey: &lastURLKey)
     }
+
     
-    /// Whether show an animating indicator when the image view is loading an image or not.
+    /// Whether show an animating activity indicator when the image view is loading an image or not.
     /// Default is false.
     public var kf_showIndicatorWhenLoading: Bool {
         get {
-            if let result = objc_getAssociatedObject(self, &showIndicatorWhenLoadingKey) as? NSNumber {
-                return result.boolValue
-            } else {
+            switch kf_indicatorType {
+            case .None:
                 return false
+            case .Activity: fallthrough
+            case .Image(_): fallthrough
+            case .Custom(_): return true
             }
         }
         
@@ -215,100 +228,70 @@ extension ImageView {
             if kf_showIndicatorWhenLoading == newValue {
                 return
             } else {
-                if newValue {
-                    
-#if os(OSX)
-                    let indicator = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
-    
-                    #if swift(>=2.3)
-                    indicator.controlSize = .Small
-                    #else
-                    indicator.controlSize = .SmallControlSize
-                    #endif
-                    indicator.style = .SpinningStyle
-#else
-    #if os(tvOS)
-                    let indicatorStyle = UIActivityIndicatorViewStyle.White
-    #else
-                    let indicatorStyle = UIActivityIndicatorViewStyle.Gray
-    #endif
-                    let indicator = UIActivityIndicatorView(activityIndicatorStyle:indicatorStyle)
-                    indicator.autoresizingMask = [.FlexibleLeftMargin, .FlexibleRightMargin, .FlexibleBottomMargin, .FlexibleTopMargin]
-#endif
-
-                    indicator.kf_center = CGPoint(x: CGRectGetMidX(bounds), y: CGRectGetMidY(bounds))
-                    indicator.hidden = true
-
-                    self.addSubview(indicator)
-                    
-                    kf_setIndicator(indicator)
-                } else {
-                    kf_indicator?.removeFromSuperview()
-                    kf_setIndicator(nil)
-                }
-                
-                objc_setAssociatedObject(self, &showIndicatorWhenLoadingKey, NSNumber(bool: newValue), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+                kf_indicatorType = .Activity
             }
         }
     }
-    
-    /// The indicator view showing when loading. This will be `nil` if `kf_showIndicatorWhenLoading` is false.
-    /// You may want to use this to set the indicator style or color when you set `kf_showIndicatorWhenLoading` to true.
-    public var kf_indicator: IndicatorView? {
-        return objc_getAssociatedObject(self, &indicatorKey) as? IndicatorView
-    }
-    
-    private func kf_setIndicator(indicator: IndicatorView?) {
-        objc_setAssociatedObject(self, &indicatorKey, indicator, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-    }
-    
-    private var kf_imageTask: RetrieveImageTask? {
-        return objc_getAssociatedObject(self, &imageTaskKey) as? RetrieveImageTask
-    }
-    
-    private func kf_setImageTask(task: RetrieveImageTask?) {
-        objc_setAssociatedObject(self, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
-    }
-}
 
+    /// Holds which indicator type is going to be used.
+    /// Default is .None
+    public var kf_indicatorType: IndicatorType {
+        get {
+            let indicator: IndicatorType? = getAssociatedObject(self, associativeKey: &indicatorTypeKey)
+            return indicator ?? .None
+        }
+        
+        set {
+            switch newValue {
+            case .None:
+                kf_indicator = nil
+            case .Activity:
+                kf_indicator = ActivityIndicator()
+            case .Image(let data):
+                kf_indicator = ImageIndicator(imageData: data)
+            case .Custom(let indicator):
+                kf_indicator = indicator
+            }
 
-extension IndicatorView {
-    func kf_startAnimating() {
-        #if os(OSX)
-            startAnimation(nil)
-        #else
-            startAnimating()
-        #endif
-        hidden = false
-    }
-    
-    func kf_stopAnimating() {
-        #if os(OSX)
-            stopAnimation(nil)
-        #else
-            stopAnimating()
-        #endif
-        hidden = true
-    }
-    
-    #if os(OSX)
-    var kf_center: CGPoint {
-    get {
-    return CGPoint(x: frame.origin.x + frame.size.width / 2.0, y: frame.origin.y + frame.size.height / 2.0 )
-    }
-    set {
-    let newFrame = CGRect(x: newValue.x - frame.size.width / 2.0, y: newValue.y - frame.size.height / 2.0, width: frame.size.width, height: frame.size.height)
-    frame = newFrame
-    }
+            setAssociatedObject(self, value: newValue, associativeKey: &indicatorTypeKey)
+        }
     }
-    #else
-    var kf_center: CGPoint {
+
+    /// `kf_indicator` holds any type that conforms to the protocol `Indicator`.
+    /// The protocol `Indicator` has a `view` property that will be shown when loading an image.
+    /// Everything will be `nil` if `kf_indicatorType` is .None.
+    public private(set) var kf_indicator: Indicator? {
         get {
-            return center
+            let indicator: (Indicator?)? = getAssociatedObject(self, associativeKey: &indicatorKey)
+            return indicator ?? nil
         }
+
         set {
-            center = newValue
+            // Remove previous
+            if let previousIndicator = kf_indicator {
+                previousIndicator.view.removeFromSuperview()
+            }
+
+            // Add new
+            if var newIndicator = newValue {
+                newIndicator.view.frame = self.frame
+                newIndicator.viewCenter = CGPoint(x: bounds.midX, y: bounds.midY)
+                newIndicator.view.hidden = true
+                self.addSubview(newIndicator.view)
+            }
+
+            // Save in associated object
+            setAssociatedObject(self,
+                                value: newValue,
+                                associativeKey: &indicatorKey)
         }
     }
-    #endif
+
+    private var kf_imageTask: RetrieveImageTask? {
+        return getAssociatedObject(self, associativeKey: &imageTaskKey)
+    }
+    
+    private func kf_setImageTask(task: RetrieveImageTask?) {
+        setAssociatedObject(self, value: task, associativeKey: &imageTaskKey)
+    }
 }

+ 176 - 0
Sources/Indicator.swift

@@ -0,0 +1,176 @@
+//
+//  Indicator.swift
+//  Kingfisher
+//
+//  Created by João D. Moreira on 30/08/16.
+//
+//  Copyright (c) 2016 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+#if os(OSX)
+    import AppKit
+#else
+    import UIKit
+#endif
+
+#if os(OSX)
+    public typealias IndicatorView = NSView
+#else
+    public typealias IndicatorView = UIView
+#endif
+
+// MARK: - Indicator Protocol
+public protocol Indicator {
+    func startAnimatingView()
+    func stopAnimatingView()
+
+    var viewCenter: CGPoint { get set }
+    var view: IndicatorView { get }
+}
+
+extension Indicator {
+    #if os(OSX)
+    var viewCenter: CGPoint {
+        get {
+            let frame = view.frame
+            return CGPoint(x: frame.origin.x + frame.size.width / 2.0, y: frame.origin.y + frame.size.height / 2.0 )
+        }
+        set {
+            let frame = view.frame
+            let newFrame = CGRect(x: newValue.x - frame.size.width / 2.0,
+                                  y: newValue.y - frame.size.height / 2.0,
+                                  width: frame.size.width,
+                                  height: frame.size.height)
+            view.frame = newFrame
+        }
+    }
+    #else
+    var viewCenter: CGPoint {
+        get {
+            return view.center
+        }
+        set {
+            view.center = newValue
+        }
+    }
+    #endif
+}
+
+// MARK: - ActivityIndicator
+// Displays a NSProgressIndicator / UIActivityIndicatorView
+struct ActivityIndicator: Indicator {
+
+    #if os(OSX)
+    private let activityIndicatorView: NSProgressIndicator
+    #else
+    private let activityIndicatorView: UIActivityIndicatorView
+    #endif
+
+    var view: IndicatorView {
+        return activityIndicatorView
+    }
+
+    func startAnimatingView() {
+        #if os(OSX)
+            activityIndicatorView.startAnimation(nil)
+        #else
+            activityIndicatorView.startAnimating()
+        #endif
+        activityIndicatorView.hidden = false
+    }
+
+    func stopAnimatingView() {
+        #if os(OSX)
+            activityIndicatorView.stopAnimation(nil)
+        #else
+            activityIndicatorView.stopAnimating()
+        #endif
+        activityIndicatorView.hidden = true
+    }
+
+    init() {
+        #if os(OSX)
+            activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
+
+            #if swift(>=2.3)
+                activityIndicatorView.controlSize = .Small
+            #else
+                activityIndicatorView.controlSize = .SmallControlSize
+            #endif
+            activityIndicatorView.style = .SpinningStyle
+        #else
+            #if os(tvOS)
+                let indicatorStyle = UIActivityIndicatorViewStyle.White
+            #else
+                let indicatorStyle = UIActivityIndicatorViewStyle.Gray
+            #endif
+            activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle:indicatorStyle)
+            activityIndicatorView.autoresizingMask = [.FlexibleLeftMargin, .FlexibleRightMargin, .FlexibleBottomMargin, .FlexibleTopMargin]
+        #endif
+    }
+}
+
+// MARK: - ImageIndicator
+// Displays an ImageView. Supports gif
+struct ImageIndicator: Indicator {
+    private let animatedImageIndicatorView: ImageView
+
+    var view: IndicatorView {
+        return animatedImageIndicatorView
+    }
+
+    init(imageData data: NSData) {
+
+        let image = Image.kf_imageWithData(data, scale: 1.0, preloadAllGIFData: true)
+        animatedImageIndicatorView = ImageView()
+        animatedImageIndicatorView.image = image
+        
+        #if os(OSX)
+            // Need for gif to animate on OSX
+            self.animatedImageIndicatorView.imageScaling = .ScaleNone
+            self.animatedImageIndicatorView.canDrawSubviewsIntoLayer = true
+        #else
+            animatedImageIndicatorView.contentMode = .Center
+            
+            animatedImageIndicatorView.autoresizingMask = [.FlexibleLeftMargin,
+                                                           .FlexibleRightMargin,
+                                                           .FlexibleBottomMargin,
+                                                           .FlexibleTopMargin]
+        #endif
+    }
+
+    func startAnimatingView() {
+        #if os(OSX)
+            animatedImageIndicatorView.animates = true
+        #else
+            animatedImageIndicatorView.startAnimating()
+        #endif
+        animatedImageIndicatorView.hidden = false
+    }
+
+    func stopAnimatingView() {
+        #if os(OSX)
+            animatedImageIndicatorView.animates = false
+        #else
+            animatedImageIndicatorView.stopAnimating()
+        #endif
+        animatedImageIndicatorView.hidden = true
+    }
+}

+ 9 - 9
Sources/NSButton+Kingfisher.swift

@@ -127,19 +127,19 @@ private var imageTaskKey: Void?
 extension NSButton {
     /// Get the image URL binded to this image view.
     public var kf_webURL: NSURL? {
-        return objc_getAssociatedObject(self, &lastURLKey) as? NSURL
+        return getAssociatedObject(self, associativeKey: &lastURLKey)
     }
 
     private func kf_setWebURL(URL: NSURL) {
-        objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: URL, associativeKey: &lastURLKey)
     }
 
     private var kf_imageTask: RetrieveImageTask? {
-        return objc_getAssociatedObject(self, &imageTaskKey) as? RetrieveImageTask
+        return getAssociatedObject(self, associativeKey: &imageTaskKey)
     }
-    
+
     private func kf_setImageTask(task: RetrieveImageTask?) {
-        objc_setAssociatedObject(self, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: task, associativeKey: &imageTaskKey)
     }
 }
 
@@ -245,19 +245,19 @@ extension NSButton {
      */
 
     public var kf_alternateWebURL: NSURL? {
-        return objc_getAssociatedObject(self, &lastAlternateURLKey) as? NSURL
+        return getAssociatedObject(self, associativeKey: &lastAlternateURLKey)
     }
 
     private func kf_setAlternateWebURL(URL: NSURL) {
-        objc_setAssociatedObject(self, &lastAlternateURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: URL, associativeKey: &lastAlternateURLKey)
     }
 
     private var kf_alternateImageTask: RetrieveImageTask? {
-        return objc_getAssociatedObject(self, &alternateImageTaskKey) as? RetrieveImageTask
+        return getAssociatedObject(self, associativeKey: &alternateImageTaskKey)
     }
 
     private func kf_setAlternateImageTask(task: RetrieveImageTask?) {
-        objc_setAssociatedObject(self, &alternateImageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: task, associativeKey: &alternateImageTaskKey)
     }
 }
 

+ 8 - 8
Sources/UIButton+Kingfisher.swift

@@ -140,7 +140,7 @@ extension UIButton {
     }
     
     private var kf_webURLs: NSMutableDictionary {
-        var dictionary = objc_getAssociatedObject(self, &lastURLKey) as? NSMutableDictionary
+        var dictionary:NSMutableDictionary? = getAssociatedObject(self, associativeKey: &lastURLKey)
         if dictionary == nil {
             dictionary = NSMutableDictionary()
             kf_setWebURLs(dictionary!)
@@ -149,15 +149,15 @@ extension UIButton {
     }
     
     private func kf_setWebURLs(URLs: NSMutableDictionary) {
-        objc_setAssociatedObject(self, &lastURLKey, URLs, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: URLs, associativeKey: &lastURLKey)
     }
     
     private var kf_imageTask: RetrieveImageTask? {
-        return objc_getAssociatedObject(self, &imageTaskKey) as? RetrieveImageTask
+        return getAssociatedObject(self, associativeKey: &imageTaskKey)
     }
     
     private func kf_setImageTask(task: RetrieveImageTask?) {
-        objc_setAssociatedObject(self, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: task, associativeKey: &imageTaskKey)
     }
 }
 
@@ -276,7 +276,7 @@ extension UIButton {
     }
     
     private var kf_backgroundWebURLs: NSMutableDictionary {
-        var dictionary = objc_getAssociatedObject(self, &lastBackgroundURLKey) as? NSMutableDictionary
+        var dictionary:NSMutableDictionary? = getAssociatedObject(self, associativeKey: &lastBackgroundURLKey)
         if dictionary == nil {
             dictionary = NSMutableDictionary()
             kf_setBackgroundWebURLs(dictionary!)
@@ -285,15 +285,15 @@ extension UIButton {
     }
     
     private func kf_setBackgroundWebURLs(URLs: NSMutableDictionary) {
-        objc_setAssociatedObject(self, &lastBackgroundURLKey, URLs, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: URLs, associativeKey: &lastBackgroundURLKey)
     }
     
     private var kf_backgroundImageTask: RetrieveImageTask? {
-        return objc_getAssociatedObject(self, &backgroundImageTaskKey) as? RetrieveImageTask
+        return getAssociatedObject(self, associativeKey: &backgroundImageTaskKey)
     }
     
     private func kf_setBackgroundImageTask(task: RetrieveImageTask?) {
-        objc_setAssociatedObject(self, &backgroundImageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        setAssociatedObject(self, value: task, associativeKey: &backgroundImageTaskKey)
     }
 }
 

+ 45 - 0
Sources/WrappedAssociatedObjects.swift

@@ -0,0 +1,45 @@
+//
+//  AssociatedObjects.swift
+//  Kingfisher
+//
+//  Created by João D. Moreira on 31/08/16.
+//  Copyright © 2016 Wei Wang. All rights reserved.
+//
+
+import ObjectiveC
+
+/**
+ *	This file provides a wrapper around Objective-C runtime associated objects.
+ *  All values are wrapped in an instance of Wrapper allowing us to persist Swift value types.
+ */
+private final class Wrapper<T> {
+    let value: T
+    init(_ x: T) {
+        value = x
+    }
+}
+
+private func wrap<T>(x: T) -> Wrapper<T> {
+    return Wrapper(x)
+}
+
+func setAssociatedObject<T>(object: AnyObject, value: T, associativeKey: UnsafePointer<Void>, policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {
+    if let v: AnyObject = value as? AnyObject {
+        objc_setAssociatedObject(object, associativeKey, v, policy)
+    } else {
+        objc_setAssociatedObject(object, associativeKey, wrap(value), policy)
+    }
+}
+
+func getAssociatedObject<T>(object: AnyObject, associativeKey: UnsafePointer<Void>) -> T? {
+
+    let v = objc_getAssociatedObject(object, associativeKey)
+
+    if let v = v as? T {
+        return v
+    } else if let v = v as? Wrapper<T> {
+        return v.value
+    } else {
+        return nil
+    }
+}

+ 8 - 8
Tests/KingfisherTests/ImageViewExtensionTests.swift

@@ -383,15 +383,15 @@ class ImageViewExtensionTests: XCTestCase {
     }
     
     func testIndicatorViewExisting() {
-        imageView.kf_showIndicatorWhenLoading = true
-        XCTAssertNotNil(imageView.kf_indicator, "The indicator view should exist when showIndicatorWhenLoading is true")
-        
-        imageView.kf_showIndicatorWhenLoading = false
-        XCTAssertNil(imageView.kf_indicator, "The indicator view should be removed when showIndicatorWhenLoading set to false")
+        imageView.kf_indicatorType = .Activity
+        XCTAssertNotNil(imageView.kf_indicator, "The indicator should exist when indicatorType is different than .None")
+
+        imageView.kf_indicatorType = .None
+        XCTAssertNil(imageView.kf_indicator, "The indicator should be removed when indicatorType is .None")
     }
     
     func testIndicatorViewAnimating() {
-        imageView.kf_showIndicatorWhenLoading = true
+        imageView.kf_indicatorType = .Activity
         
         let expectation = expectationWithDescription("wait for downloading image")
         
@@ -403,11 +403,11 @@ class ImageViewExtensionTests: XCTestCase {
             
             let indicator = self.imageView.kf_indicator
             XCTAssertNotNil(indicator, "The indicator view should exist when showIndicatorWhenLoading is true")
-            XCTAssertFalse(indicator!.hidden, "The indicator should be shown and animating when loading")
+            XCTAssertFalse(indicator!.view.hidden, "The indicator should be shown and animating when loading")
 
         }) { (image, error, cacheType, imageURL) -> () in
             let indicator = self.imageView.kf_indicator
-            XCTAssertTrue(indicator!.hidden, "The indicator should stop and hidden after loading")
+            XCTAssertTrue(indicator!.view.hidden, "The indicator should stop and hidden after loading")
             expectation.fulfill()
         }
         

BIN
images/loader.gif