Browse Source

Merge branch 'master' into xcode11

# Conflicts:
#	Kingfisher.xcodeproj/project.pbxproj
#	Sources/Cache/CacheSerializer.swift
#	Sources/Views/AnimatedImageView.swift
#	Tests/KingfisherTests-macOS/Info.plist
#	Tests/KingfisherTests-tvOS/Info.plist
onevcat 6 years ago
parent
commit
e2277c840f

+ 4 - 0
.github/FUNDING.yml

@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+# github: [onevcat]
+open_collective: kingfisher

+ 24 - 0
CHANGELOG.md

@@ -2,6 +2,30 @@
 
 -----
 
+## [5.7.1 - Thread Things](https://github.com/onevcat/Kingfisher/releases/tag/5.7.1) (2019-08-11)
+
+#### Fix
+* Setting `runLoopMode` for `AnimatedImageView` will trigger animation restart normally. [#1253](https://github.com/onevcat/Kingfisher/pull/1253)
+* A possible thread issue when removing storage object from memory cache by the cache policy. [#1255](https://github.com/onevcat/Kingfisher/pull/1255)
+* Manipulating on `AnimateImageView`'s frame array is now thread safe. [#1257](https://github.com/onevcat/Kingfisher/pull/1257)
+
+---
+
+## [5.7.0 - Summer Bird](https://github.com/onevcat/Kingfisher/releases/tag/5.7.0) (2019-07-03)
+
+#### Add
+* Mark `cacheFileURL(forKey:)` of `DiskStorage` to public. [#1214](https://github.com/onevcat/Kingfisher/issues/1214)
+* Mark `KingfisherManager` initializer to public so other dependencies can customize the manager behavior. [#1216](https://github.com/onevcat/Kingfisher/issues/1216)
+
+#### Fix
+* Performance improvement on progressive JPEG scanning. [#1218](https://github.com/onevcat/Kingfisher/pull/1218)
+* Fix a potential thread issue when checking progressive JPEG. [#1220](https://github.com/onevcat/Kingfisher/pull/1220)
+
+#### Remove
+* The deprecated `Result` extensions for Swift 4 back compatibility are removed. [#1224](https://github.com/onevcat/Kingfisher/pull/1224)
+
+---
+
 ## [5.6.0 - The Sands of Time](https://github.com/onevcat/Kingfisher/releases/tag/5.6.0) (2019-06-11)
 
 #### Add

+ 1 - 1
Demo/Demo/Kingfisher-Demo/Base.lproj/LaunchScreen.xib

@@ -15,7 +15,7 @@
             <rect key="frame" x="0.0" y="0.0" width="480" height="480"/>
             <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
             <subviews>
-                <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="  Copyright (c) 2018年 Wei Wang. All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
+                <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="  Copyright (c) 2019 Wei Wang (onevcat). All rights reserved." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="9" translatesAutoresizingMaskIntoConstraints="NO" id="8ie-xW-0ye">
                     <rect key="frame" x="20" y="439" width="441" height="21"/>
                     <fontDescription key="fontDescription" type="system" pointSize="17"/>
                     <color key="textColor" cocoaTouchSystemColor="darkTextColor"/>

+ 1 - 1
Gemfile

@@ -4,4 +4,4 @@ source "https://rubygems.org"
 
 gem "fastlane"
 gem "jazzy"
-gem "cocoapods", "~> 1.7.1"
+gem "cocoapods", "~> 1.8.beta"

+ 30 - 25
Gemfile.lock

@@ -9,13 +9,16 @@ GEM
       tzinfo (~> 1.1)
     addressable (2.6.0)
       public_suffix (>= 2.0.2, < 4.0)
+    algoliasearch (1.26.1)
+      httpclient (~> 2.8, >= 2.8.3)
+      json (>= 1.5.1)
     atomos (0.1.3)
     babosa (1.0.2)
-    claide (1.0.2)
-    cocoapods (1.7.1)
+    claide (1.0.3)
+    cocoapods (1.8.0.beta.1)
       activesupport (>= 4.0.2, < 5)
       claide (>= 1.0.2, < 2.0)
-      cocoapods-core (= 1.7.1)
+      cocoapods-core (= 1.8.0.beta.1)
       cocoapods-deintegrate (>= 1.0.3, < 2.0)
       cocoapods-downloader (>= 1.2.2, < 2.0)
       cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -25,14 +28,15 @@ GEM
       cocoapods-try (>= 1.1.0, < 2.0)
       colored2 (~> 3.1)
       escape (~> 0.0.4)
-      fourflusher (>= 2.2.0, < 3.0)
+      fourflusher (>= 2.3.0, < 3.0)
       gh_inspector (~> 1.0)
       molinillo (~> 0.6.6)
       nap (~> 1.0)
       ruby-macho (~> 1.4)
-      xcodeproj (>= 1.8.2, < 2.0)
-    cocoapods-core (1.7.1)
+      xcodeproj (>= 1.11.1, < 2.0)
+    cocoapods-core (1.8.0.beta.1)
       activesupport (>= 4.0.2, < 6)
+      algoliasearch (~> 1.0)
       fuzzy_match (~> 2.0.4)
       nap (~> 1.0)
     cocoapods-deintegrate (1.0.4)
@@ -53,12 +57,12 @@ GEM
     declarative (0.0.10)
     declarative-option (0.1.0)
     digest-crc (0.4.1)
-    domain_name (0.5.20180417)
+    domain_name (0.5.20190701)
       unf (>= 0.0.5, < 1.0.0)
-    dotenv (2.7.2)
+    dotenv (2.7.5)
     emoji_regex (1.0.1)
     escape (0.0.4)
-    excon (0.64.0)
+    excon (0.66.0)
     faraday (0.15.4)
       multipart-post (>= 1.2, < 3)
     faraday-cookie_jar (0.0.6)
@@ -67,7 +71,7 @@ GEM
     faraday_middleware (0.13.1)
       faraday (>= 0.7.4, < 1.0)
     fastimage (2.1.5)
-    fastlane (2.124.0)
+    fastlane (2.128.1)
       CFPropertyList (>= 2.3, < 4.0.0)
       addressable (>= 2.3, < 3.0.0)
       babosa (>= 1.0.2, < 2.0.0)
@@ -86,7 +90,8 @@ GEM
       google-cloud-storage (>= 1.15.0, < 2.0.0)
       highline (>= 1.7.2, < 2.0.0)
       json (< 3.0.0)
-      mini_magick (~> 4.5.1)
+      jwt (~> 2.1.0)
+      mini_magick (>= 4.9.4, < 5.0.0)
       multi_xml (~> 0.5)
       multipart-post (~> 2.0.0)
       plist (>= 3.1.0, < 4.0.0)
@@ -104,7 +109,7 @@ GEM
       xcpretty (~> 0.3.0)
       xcpretty-travis-formatter (>= 0.0.3)
     ffi (1.11.1)
-    fourflusher (2.3.0)
+    fourflusher (2.3.1)
     fuzzy_match (2.0.4)
     gh_inspector (1.1.3)
     google-api-client (0.23.9)
@@ -117,7 +122,7 @@ GEM
       signet (~> 0.9)
     google-cloud-core (1.3.0)
       google-cloud-env (~> 1.0)
-    google-cloud-env (1.0.5)
+    google-cloud-env (1.2.0)
       faraday (~> 0.11)
     google-cloud-storage (1.16.0)
       digest-crc (~> 0.4)
@@ -137,29 +142,29 @@ GEM
     httpclient (2.8.3)
     i18n (0.9.5)
       concurrent-ruby (~> 1.0)
-    jazzy (0.9.4)
-      cocoapods (~> 1.0)
-      mustache (~> 0.99)
+    jazzy (0.10.0)
+      cocoapods (~> 1.5)
+      mustache (~> 1.1)
       open4
-      redcarpet (~> 3.2)
+      redcarpet (~> 3.4)
       rouge (>= 2.0.6, < 4.0)
-      sass (~> 3.4)
+      sass (~> 3.6)
       sqlite3 (~> 1.3)
       xcinvoke (~> 0.3.0)
     json (2.2.0)
-    jwt (2.2.1)
+    jwt (2.1.0)
     liferaft (0.0.6)
     memoist (0.16.0)
     mime-types (3.2.2)
       mime-types-data (~> 3.2015)
     mime-types-data (3.2019.0331)
-    mini_magick (4.5.1)
+    mini_magick (4.9.5)
     minitest (5.11.3)
     molinillo (0.6.6)
     multi_json (1.13.1)
     multi_xml (0.6.0)
     multipart-post (2.0.0)
-    mustache (0.99.8)
+    mustache (1.1.0)
     nanaimo (0.2.6)
     nap (1.1.0)
     naturally (2.2.0)
@@ -171,7 +176,7 @@ GEM
     rb-fsevent (0.10.3)
     rb-inotify (0.10.0)
       ffi (~> 1.0)
-    redcarpet (3.4.0)
+    redcarpet (3.5.0)
     representable (3.0.4)
       declarative (< 0.1.0)
       declarative-option (< 0.2.0)
@@ -214,7 +219,7 @@ GEM
     word_wrap (1.0.0)
     xcinvoke (0.3.0)
       liferaft (~> 0.0.6)
-    xcodeproj (1.9.0)
+    xcodeproj (1.12.0)
       CFPropertyList (>= 2.3.3, < 4.0)
       atomos (~> 0.1.3)
       claide (>= 1.0.2, < 2.0)
@@ -229,9 +234,9 @@ PLATFORMS
   ruby
 
 DEPENDENCIES
-  cocoapods (~> 1.7.1)
+  cocoapods (~> 1.8.beta)
   fastlane
   jazzy
 
 BUNDLED WITH
-   2.0.1
+   2.0.2

+ 1 - 1
Kingfisher.podspec

@@ -1,7 +1,7 @@
 Pod::Spec.new do |s|
 
   s.name         = "Kingfisher"
-  s.version      = "5.6.0"
+  s.version      = "5.7.1"
   s.summary      = "A lightweight and pure Swift implemented library for downloading and cacheing image from the web."
 
   s.description  = <<-DESC

+ 2 - 2
Kingfisher.xcodeproj/xcshareddata/xcschemes/KingfisherSwiftUI.xcscheme

@@ -15,7 +15,7 @@
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "D1F7603B230974DE000C5269"
-               BuildableName = "Kingfisher.framework"
+               BuildableName = "KingfisherSwiftUI.framework"
                BlueprintName = "KingfisherSwiftUI"
                ReferencedContainer = "container:Kingfisher.xcodeproj">
             </BuildableReference>
@@ -51,7 +51,7 @@
          <BuildableReference
             BuildableIdentifier = "primary"
             BlueprintIdentifier = "D1F7603B230974DE000C5269"
-            BuildableName = "Kingfisher.framework"
+            BuildableName = "KingfisherSwiftUI.framework"
             BlueprintName = "KingfisherSwiftUI"
             ReferencedContainer = "container:Kingfisher.xcodeproj">
          </BuildableReference>

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2018 Wei Wang
+Copyright (c) 2019 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

+ 0 - 2
Sources/Cache/CacheSerializer.swift

@@ -93,8 +93,6 @@ public struct DefaultCacheSerializer: CacheSerializer {
     /// - Note:
     /// Only when `original` contains valid PNG, JPEG and GIF format data, the `image` will be
     /// converted to the corresponding data type. Otherwise, if the `original` is provided but it is not
-    /// a valid format, the `original` data will be used for cache.
-    ///
     /// If `original` is `nil`, the input `image` will be encoded as PNG data.
     public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
         return image.kf.data(format: original?.kf.imageFormat ?? .unknown)

+ 9 - 1
Sources/Cache/DiskStorage.swift

@@ -190,7 +190,15 @@ public enum DiskStorage {
             }
         }
 
-        func cacheFileURL(forKey key: String) -> URL {
+        /// The URL of the cached file with a given computed `key`.
+        ///
+        /// - Note:
+        /// This method does not guarantee there is an image already cached in the returned URL. It just gives your
+        /// the URL that the image should be if it exists in disk storage, with the give key.
+        ///
+        /// - Parameter key: The final computed key used when caching the image. Please note that usually this is not
+        /// the `cacheKey` of an image `Source`. It is the computed key with processor identifier considered.
+        public func cacheFileURL(forKey key: String) -> URL {
             let fileName = cacheFileName(forKey: key)
             return directoryURL.appendingPathComponent(fileName)
         }

+ 12 - 17
Sources/Cache/MemoryStorage.swift

@@ -43,12 +43,17 @@ public enum MemoryStorage {
     /// items from memory.
     public class Backend<T: CacheCostCalculable> {
         let storage = NSCache<NSString, StorageObject<T>>()
-        var keys = Set<String>()
 
-        var cleanTimer: Timer? = nil
-        let lock = NSLock()
+        // Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding
+        // key would be also removed. However, for the object removing triggered by cache rule/policy of system, the
+        // key will be remained there until next `removeExpired` happens.
+        //
+        // Breaking the strict tracking could save additional locking behaviors.
+        // See https://github.com/onevcat/Kingfisher/issues/1233
+        var keys = Set<String>()
 
-        let cacheDelegate = CacheDelegate<StorageObject<T>>()
+        private var cleanTimer: Timer? = nil
+        private let lock = NSLock()
 
         /// The config used in this storage. It is a value you can set and
         /// use to config the storage in air.
@@ -67,10 +72,6 @@ public enum MemoryStorage {
             self.config = config
             storage.totalCostLimit = config.totalCostLimit
             storage.countLimit = config.countLimit
-            storage.delegate = cacheDelegate
-            cacheDelegate.onObjectRemoved.delegate(on: self) { (self, obj) in
-                self.keys.remove(obj.key)
-            }
 
             cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
                 guard let self = self else { return }
@@ -84,6 +85,9 @@ public enum MemoryStorage {
             for key in keys {
                 let nsKey = key as NSString
                 guard let object = storage.object(forKey: nsKey) else {
+                    // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule.
+                    // We didn't remove the key yet until now, since we do not want to introduce additonal lock.
+                    // See https://github.com/onevcat/Kingfisher/issues/1233
                     keys.remove(key)
                     continue
                 }
@@ -163,15 +167,6 @@ public enum MemoryStorage {
             storage.removeAllObjects()
             keys.removeAll()
         }
-
-        class CacheDelegate<T>: NSObject, NSCacheDelegate {
-            let onObjectRemoved = Delegate<T, Void>()
-            func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) {
-                if let obj = obj as? T {
-                    onObjectRemoved.call(obj)
-                }
-            }
-        }
     }
 }
 

+ 6 - 8
Sources/Extensions/UIButton+Kingfisher.swift

@@ -332,10 +332,9 @@ extension KingfisherWrapper where Base: UIButton {
     
     private var taskIdentifierInfo: TaskIdentifier {
         return  getAssociatedObject(base, &taskIdentifierKey) ?? {
-            let value = TaskIdentifier([:])
-            setRetainedAssociatedObject(base, &taskIdentifierKey, value)
-            return value
-        }()
+            setRetainedAssociatedObject(base, &taskIdentifierKey, $0)
+            return $0
+        } (TaskIdentifier([:]))
     }
     
     private var imageTask: DownloadTask? {
@@ -361,10 +360,9 @@ extension KingfisherWrapper where Base: UIButton {
     
     private var backgroundTaskIdentifierInfo: TaskIdentifier {
         return  getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? {
-            let value = TaskIdentifier([:])
-            setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, value)
-            return value
-        }()
+            setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0)
+            return $0
+        } (TaskIdentifier([:]))
     }
     
     private var backgroundImageTask: DownloadTask? {

+ 7 - 2
Sources/General/KingfisherManager.swift

@@ -84,8 +84,13 @@ public class KingfisherManager {
     private convenience init() {
         self.init(downloader: .default, cache: .default)
     }
-    
-    init(downloader: ImageDownloader, cache: ImageCache) {
+
+    /// Creates an image setting manager with specified downloader and cache.
+    ///
+    /// - Parameters:
+    ///   - downloader: The image downloader used to download images.
+    ///   - cache: The image cache which stores memory and disk images.
+    public init(downloader: ImageDownloader, cache: ImageCache) {
         self.downloader = downloader
         self.cache = cache
 

+ 1 - 1
Sources/General/KingfisherOptionsInfo.swift

@@ -211,7 +211,7 @@ public enum KingfisherOptionsInfoItem {
     /// To disable extending option at all add memoryCacheAccessExtendingExpiration(.none) to options.
     case memoryCacheAccessExtendingExpiration(ExpirationExtending)
     
-    /// The expiration setting for memory cache. By default, the underlying `DiskStorage.Backend` uses the
+    /// The expiration setting for disk cache. By default, the underlying `DiskStorage.Backend` uses the
     /// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated
     /// value to overwrite the config setting for this caching item.
     case diskCacheExpiration(StorageExpiration)

+ 39 - 34
Sources/Image/ImageProgressive.swift

@@ -71,7 +71,7 @@ final class ImageProgressiveProvider: DataReceivingSideEffect {
     private let refresh: (KFCrossPlatformImage) -> Void
     
     private let decoder: ImageProgressiveDecoder
-    private let queue = ImageProgressiveSerialQueue(.main)
+    private let queue = ImageProgressiveSerialQueue()
     
     init?(_ options: KingfisherParsedOptionsInfo,
           refresh: @escaping (KFCrossPlatformImage) -> Void) {
@@ -89,16 +89,14 @@ final class ImageProgressiveProvider: DataReceivingSideEffect {
     func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) {
         guard !data.isEmpty else { return }
         
-        let interval = option.scanInterval
-        let isFastest = option.isFastestScan
-        
-        func add(decode data: Data) {
-            queue.add(minimum: interval) { completion in
-                guard self.onShouldApply() else {
-                    self.queue.clean()
-                    completion()
-                    return
-                }
+        queue.add(minimum: option.scanInterval) { completion in
+            guard self.onShouldApply() else {
+                self.queue.clean()
+                completion()
+                return
+            }
+            
+            func decode(_ data: Data) {
                 self.decoder.decode(data, with: callbacks) { image in
                     defer { completion() }
                     guard self.onShouldApply() else { return }
@@ -106,13 +104,13 @@ final class ImageProgressiveProvider: DataReceivingSideEffect {
                     self.refresh(image)
                 }
             }
-        }
-        
-        if isFastest {
-            add(decode: decoder.scanning(data) ?? Data())
             
-        } else {
-            decoder.scanning(data).forEach { add(decode: $0) }
+            if self.option.isFastestScan {
+                decode(self.decoder.scanning(data) ?? Data())
+                
+            } else {
+                self.decoder.scanning(data).forEach { decode($0) }
+            }
         }
     }
 }
@@ -252,19 +250,18 @@ private final class ImageProgressiveDecoder {
 private final class ImageProgressiveSerialQueue {
     typealias ClosureCallback = ((@escaping () -> Void)) -> Void
     
-    private let queue: DispatchQueue
+    private let queue: DispatchQueue = .init(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue")
     private var items: [DispatchWorkItem] = []
     private var notify: (() -> Void)?
     private var lastTime: TimeInterval?
     var count: Int { return items.count }
     
-    init(_ queue: DispatchQueue) {
-        self.queue = queue
-    }
-    
     func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) {
-        let completion = {
-            self.queue.async {
+        let completion = { [weak self] in
+            guard let self = self else { return }
+            
+            self.queue.async { [weak self] in
+                guard let self = self else { return }
                 guard !self.items.isEmpty else { return }
                 
                 self.items.removeFirst()
@@ -282,15 +279,20 @@ private final class ImageProgressiveSerialQueue {
                 }
             }
         }
-        let item = DispatchWorkItem {
-            closure(completion)
-        }
-        if items.isEmpty {
-            let difference = Date().timeIntervalSince1970 - (lastTime ?? 0)
-            let delay = difference < interval ? interval - difference : 0
-            queue.asyncAfter(deadline: .now() + delay, execute: item)
+        
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            
+            let item = DispatchWorkItem {
+                closure(completion)
+            }
+            if self.items.isEmpty {
+                let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0)
+                let delay = difference < interval ? interval - difference : 0
+                self.queue.asyncAfter(deadline: .now() + delay, execute: item)
+            }
+            self.items.append(item)
         }
-        items.append(item)
     }
     
     func notify(_ closure: @escaping () -> Void) {
@@ -298,7 +300,10 @@ private final class ImageProgressiveSerialQueue {
     }
     
     func clean() {
-        items.forEach { $0.cancel() }
-        items.removeAll()
+        queue.async { [weak self] in
+            guard let self = self else { return }
+            self.items.forEach { $0.cancel() }
+            self.items.removeAll()
+        }
     }
 }

+ 2 - 2
Sources/Info.plist

@@ -15,11 +15,11 @@
 	<key>CFBundlePackageType</key>
 	<string>FMWK</string>
 	<key>CFBundleShortVersionString</key>
-	<string>5.6.0</string>
+	<string>5.7.1</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleVersion</key>
-	<string>1687</string>
+	<string>1725</string>
 	<key>NSPrincipalClass</key>
 	<string></string>
 </dict>

+ 10 - 2
Sources/Networking/ImageDownloader.swift

@@ -193,8 +193,17 @@ open class ImageDownloader {
         }
     }
 
+    // MARK: Dowloading Task
+    /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
+    ///
+    /// - Parameters:
+    ///   - url: Target URL.
+    ///   - options: The options could control download behavior. See `KingfisherOptionsInfo`.
+    ///   - completionHandler: Called when the download progress finishes. This block will be called in the queue
+    ///                        defined in `.callbackQueue` in `options` parameter.
+    /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
     @discardableResult
-    func downloadImage(
+    open func downloadImage(
         with url: URL,
         options: KingfisherParsedOptionsInfo,
         completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
@@ -312,7 +321,6 @@ open class ImageDownloader {
         return downloadTask
     }
 
-    // MARK: Dowloading Task
     /// Downloads an image with a URL and option.
     ///
     /// - Parameters:

+ 0 - 37
Sources/Utility/Result.swift

@@ -194,43 +194,6 @@ extension Result : CustomDebugStringConvertible {
 }
 #endif
 
-// Deprecated
-extension Result {
-
-    /// The stored value of a successful `Result`. `nil` if the `Result` was a failure.
-    @available(*, deprecated, message: "This method will be removed soon. Use `get() throws -> Success` instead.")
-    public var value: Success? {
-        switch self {
-        case let .success(value):
-            return value
-        case .failure:
-            return nil
-        }
-    }
-
-    /// The stored value of a failure `Result`. `nil` if the `Result` was a success.
-    @available(*, deprecated, message: "This method will be removed soon. Use `get() throws -> Success` instead.")
-    public var error: Failure? {
-        switch self {
-        case let .failure(error):
-            return error
-        case .success:
-            return nil
-        }
-    }
-
-    /// A Boolean value indicating whether the `Result` as a success.
-    @available(*, deprecated, message: "This method will be removed soon. Use methods defined in `Swift.Result`.")
-    public var isSuccess: Bool {
-        switch self {
-        case .success:
-            return true
-        case .failure:
-            return false
-        }
-    }
-}
-
 // These helper methods are not public since we do not want them to be exposed or cause any conflicting.
 // However, they are just wrapper of `ResultUtil` static methods.
 extension Result where Failure: Error {

+ 51 - 12
Sources/Views/AnimatedImageView.swift

@@ -131,7 +131,7 @@ open class AnimatedImageView: UIImageView {
     /// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling.
     public var runLoopMode = KFRunLoopModeCommon {
         willSet {
-            guard runLoopMode == newValue else { return }
+            guard runLoopMode != newValue else { return }
             stopAnimating()
             displayLink.remove(from: .main, forMode: runLoopMode)
             displayLink.add(to: .main, forMode: newValue)
@@ -361,7 +361,7 @@ extension AnimatedImageView {
         private let maxRepeatCount: RepeatCount
 
         private let maxTimeStep: TimeInterval = 1.0
-        private var animatedFrames = [AnimatedFrame]()
+        private let animatedFrames = SafeArray<AnimatedFrame>()
         private var frameCount = 0
         private var timeSinceLastFrameChange: TimeInterval = 0.0
         private var currentRepeatCount: UInt = 0
@@ -450,11 +450,11 @@ extension AnimatedImageView {
         }
 
         func frame(at index: Int) -> KFCrossPlatformImage? {
-            return animatedFrames[safe: index]?.image
+            return animatedFrames[index]?.image
         }
 
         func duration(at index: Int) -> TimeInterval {
-            return animatedFrames[safe: index]?.duration  ?? .infinity
+            return animatedFrames[index]?.duration  ?? .infinity
         }
 
         func prepareFramesAsynchronously() {
@@ -485,17 +485,17 @@ extension AnimatedImageView {
             (0..<frameCount).forEach { index in
                 let frameDuration = GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)
                 duration += min(frameDuration, maxTimeStep)
-                animatedFrames += [AnimatedFrame(image: nil, duration: frameDuration)]
+                animatedFrames.append(AnimatedFrame(image: nil, duration: frameDuration))
 
                 if index > maxFrameCount { return }
-                animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(image: loadFrame(at: index))
+                animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index))
             }
 
             self.loopDuration = duration
         }
 
         private func resetAnimatedFrames() {
-            animatedFrames = []
+            animatedFrames.removeAll()
         }
 
         private func loadFrame(at index: Int) -> UIImage? {
@@ -522,10 +522,10 @@ extension AnimatedImageView {
                 return
             }
 
-            animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame
+            animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex]?.placeholderFrame
 
             preloadIndexes(start: currentFrameIndex).forEach { index in
-                let currentAnimatedFrame = animatedFrames[index]
+                guard let currentAnimatedFrame = animatedFrames[index] else { return }
                 if !currentAnimatedFrame.isPlaceholder { return }
                 animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index))
             }
@@ -566,9 +566,48 @@ extension AnimatedImageView {
     }
 }
 
-extension Array {
-    subscript(safe index: Int) -> Element? {
-        return indices ~= index ? self[index] : nil
+class SafeArray<Element> {
+    private var array: Array<Element> = []
+    private let lock = NSLock()
+    
+    subscript(index: Int) -> Element? {
+        get {
+            lock.lock()
+            defer { lock.unlock() }
+            return array.indices ~= index ? array[index] : nil
+        }
+        
+        set {
+            lock.lock()
+            defer { lock.unlock() }
+            if let newValue = newValue, array.indices ~= index {
+                array[index] = newValue
+            }
+        }
+    }
+    
+    var count : Int {
+        lock.lock()
+        defer { lock.unlock() }
+        return array.count
+    }
+    
+    func reserveCapacity(_ count: Int) {
+        lock.lock()
+        defer { lock.unlock() }
+        array.reserveCapacity(count)
+    }
+    
+    func append(_ element: Element) {
+        lock.lock()
+        defer { lock.unlock() }
+        array += [element]
+    }
+    
+    func removeAll() {
+        lock.lock()
+        defer { lock.unlock() }
+        array = []
     }
 }
 #endif

+ 2 - 2
Tests/KingfisherTests/Info.plist

@@ -15,10 +15,10 @@
 	<key>CFBundlePackageType</key>
 	<string>BNDL</string>
 	<key>CFBundleShortVersionString</key>
-	<string>5.6.0</string>
+	<string>5.7.1</string>
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleVersion</key>
-	<string>1687</string>
+	<string>1725</string>
 </dict>
 </plist>

+ 1 - 1
Tests/KingfisherTests/StringExtensionTests.swift

@@ -3,7 +3,7 @@
 //  Kingfisher
 //
 //  Created by Wei Wang on 16/8/14.
-//  Copyright © 2018年 Wei Wang. All rights reserved.
+//  Copyright © 2019 Wei Wang. All rights reserved.
 //
 
 import XCTest

+ 1 - 0
fastlane/Fastfile

@@ -89,6 +89,7 @@ platform :ios do
       target_version = options[:version]
       Actions.sh("cd .. && jazzy \
                         --clean \
+                        -x USE_SWIFT_RESPONSE_FILE=NO \
                         --author \"Wei Wang\" \
                         --author_url https://onevcat.com \
                         --github_url https://github.com/onevcat/Kingfisher \