diff --git a/Demo/Kingfisher-Demo/ViewController.swift b/Demo/Kingfisher-Demo/ViewController.swift index e7181bc85..cfed8e145 100644 --- a/Demo/Kingfisher-Demo/ViewController.swift +++ b/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 diff --git a/Demo/Kingfisher-OSX-Demo/ViewController.swift b/Demo/Kingfisher-OSX-Demo/ViewController.swift index 8aa763beb..577e84455 100644 --- a/Demo/Kingfisher-OSX-Demo/ViewController.swift +++ b/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)") diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index 808fec2c9..56f633e9e 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/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 = ""; }; 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 = ""; }; + CD2C73531D7F040B00A1D819 /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Indicator.swift; sourceTree = ""; }; + 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 = ""; }; D10945EA1C526B6C001408EB /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image.swift; sourceTree = ""; }; D10945EB1C526B6C001408EB /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/ImageCache.swift; sourceTree = ""; }; D10945EC1C526B6C001408EB /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/ImageDownloader.swift; sourceTree = ""; }; @@ -459,6 +472,8 @@ D10945F51C526B6C001408EB /* ThreadHelper.swift */, D10945F61C526B6C001408EB /* UIButton+Kingfisher.swift */, 182FFF771CC9ACBA004B728D /* NSButton+Kingfisher.swift */, + CD4593961D7EAB9900FD570E /* WrappedAssociatedObjects.swift */, + CD2C73531D7F040B00A1D819 /* Indicator.swift */, ); name = Sources; sourceTree = ""; @@ -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; diff --git a/Sources/Image.swift b/Sources/Image.swift index 470f12ff2..6ea6cce2b 100644 --- a/Sources/Image.swift +++ b/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 diff --git a/Sources/ImageView+Kingfisher.swift b/Sources/ImageView+Kingfisher.swift index 4b4bd824e..63d80036e 100644 --- a/Sources/ImageView+Kingfisher.swift +++ b/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) + } } diff --git a/Sources/Indicator.swift b/Sources/Indicator.swift new file mode 100644 index 000000000..2d05e9659 --- /dev/null +++ b/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 +// +// 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 + } +} diff --git a/Sources/NSButton+Kingfisher.swift b/Sources/NSButton+Kingfisher.swift index 3e686da41..57c9fbb27 100644 --- a/Sources/NSButton+Kingfisher.swift +++ b/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) } } diff --git a/Sources/UIButton+Kingfisher.swift b/Sources/UIButton+Kingfisher.swift index 4453af650..d37ac1b14 100644 --- a/Sources/UIButton+Kingfisher.swift +++ b/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) } } diff --git a/Sources/WrappedAssociatedObjects.swift b/Sources/WrappedAssociatedObjects.swift new file mode 100644 index 000000000..d076b7ba3 --- /dev/null +++ b/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 { + let value: T + init(_ x: T) { + value = x + } +} + +private func wrap(x: T) -> Wrapper { + return Wrapper(x) +} + +func setAssociatedObject(object: AnyObject, value: T, associativeKey: UnsafePointer, 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(object: AnyObject, associativeKey: UnsafePointer) -> T? { + + let v = objc_getAssociatedObject(object, associativeKey) + + if let v = v as? T { + return v + } else if let v = v as? Wrapper { + return v.value + } else { + return nil + } +} diff --git a/Tests/KingfisherTests/ImageViewExtensionTests.swift b/Tests/KingfisherTests/ImageViewExtensionTests.swift index a70ecacae..ffd89e214 100644 --- a/Tests/KingfisherTests/ImageViewExtensionTests.swift +++ b/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() } diff --git a/images/loader.gif b/images/loader.gif new file mode 100644 index 000000000..ed2c01b5e Binary files /dev/null and b/images/loader.gif differ