Parcourir la source

Merge branch 'master' of https://github.com/MxIris-Library-Forks/Kingfisher into MxIris-Library-Forks-master

onevcat il y a 1 an
Parent
commit
a2fdda2d6a

+ 4 - 70
Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard

@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
     <dependencies>
-        <deployment identifier="macosx"/>
         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22505"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
@@ -670,10 +669,10 @@
             </objects>
             <point key="canvasLocation" x="75" y="250"/>
         </scene>
-        <!--View Controller-->
+        <!--Common View Controller-->
         <scene sceneID="hIz-AP-VOD">
             <objects>
-                <viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Kingfisher_macOS_Demo" customModuleProvider="target" sceneMemberID="viewController">
+                <viewController id="XfG-lQ-9wD" customClass="CommonViewController" customModule="Kingfisher_macOS_Demo" customModuleProvider="target" sceneMemberID="viewController">
                     <view key="view" id="m2S-Jp-Qdl">
                         <rect key="frame" x="0.0" y="0.0" width="620" height="480"/>
                         <autoresizingMask key="autoresizingMask"/>
@@ -682,7 +681,7 @@
                                 <rect key="frame" x="0.0" y="0.0" width="620" height="450"/>
                                 <clipView key="contentView" id="rM7-g8-q6C">
                                     <rect key="frame" x="1" y="1" width="618" height="448"/>
-                                    <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                                    <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <collectionView id="XnK-6H-mn7">
                                             <rect key="frame" x="0.0" y="0.0" width="618" height="448"/>
@@ -717,16 +716,6 @@
                                     <action selector="clearCachePressedWithSender:" target="XfG-lQ-9wD" id="lfl-RU-nX5"/>
                                 </connections>
                             </button>
-                            <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="eLA-Ce-crP" userLabel="Heavy GIFs">
-                                <rect key="frame" x="259" y="448" width="103" height="32"/>
-                                <buttonCell key="cell" type="push" title="Heavy GIFs" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="YYz-yg-1MB">
-                                    <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
-                                    <font key="font" metaFont="system"/>
-                                </buttonCell>
-                                <connections>
-                                    <segue destination="19n-lE-hND" kind="show" id="NZO-24-0LI"/>
-                                </connections>
-                            </button>
                             <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ejh-qu-qmy">
                                 <rect key="frame" x="531" y="448" width="76" height="32"/>
                                 <buttonCell key="cell" type="push" title="Reload" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="DhD-Tg-Bw3">
@@ -737,31 +726,16 @@
                                     <action selector="reloadPressedWithSender:" target="XfG-lQ-9wD" id="k24-Wi-NRd"/>
                                 </connections>
                             </button>
-                            <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0Du-le-fYN">
-                                <rect key="frame" x="417" y="448" width="78" height="32"/>
-                                <buttonCell key="cell" type="push" title="SwiftUI" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="wIi-ia-bgi">
-                                    <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
-                                    <font key="font" metaFont="system"/>
-                                </buttonCell>
-                                <connections>
-                                    <segue destination="ei1-kq-tvV" kind="show" id="Ub0-4z-BqC"/>
-                                </connections>
-                            </button>
                         </subviews>
                         <constraints>
                             <constraint firstAttribute="trailing" secondItem="MlO-xV-qug" secondAttribute="trailing" id="18w-Qc-Jr6"/>
                             <constraint firstItem="yIr-uo-Quc" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="5" id="35L-th-6RE"/>
-                            <constraint firstItem="Ejh-qu-qmy" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="eLA-Ce-crP" secondAttribute="trailing" constant="12" symbolic="YES" id="7TA-fc-yjU"/>
                             <constraint firstItem="yIr-uo-Quc" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" constant="20" id="AhA-g2-Cms"/>
-                            <constraint firstItem="eLA-Ce-crP" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="yIr-uo-Quc" secondAttribute="trailing" constant="12" symbolic="YES" id="BRb-HY-Dny"/>
+                            <constraint firstItem="Ejh-qu-qmy" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="yIr-uo-Quc" secondAttribute="trailing" constant="12" symbolic="YES" id="BRb-HY-Dny"/>
                             <constraint firstItem="MlO-xV-qug" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="30" id="Bzu-9v-r7G"/>
-                            <constraint firstItem="0Du-le-fYN" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="5" id="DMs-p7-JWV"/>
                             <constraint firstAttribute="bottom" secondItem="MlO-xV-qug" secondAttribute="bottom" id="HY0-vM-k0l"/>
                             <constraint firstItem="MlO-xV-qug" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" id="Pp3-O7-2Bs"/>
-                            <constraint firstItem="eLA-Ce-crP" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="5" id="cJE-Zj-BEy"/>
                             <constraint firstAttribute="trailing" secondItem="Ejh-qu-qmy" secondAttribute="trailing" constant="20" id="eoW-Xb-6wq"/>
-                            <constraint firstItem="eLA-Ce-crP" firstAttribute="centerX" secondItem="m2S-Jp-Qdl" secondAttribute="centerX" id="xRm-9b-mgK"/>
-                            <constraint firstItem="Ejh-qu-qmy" firstAttribute="leading" secondItem="0Du-le-fYN" secondAttribute="trailing" constant="50" id="xc2-W0-bu0"/>
                             <constraint firstItem="Ejh-qu-qmy" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" constant="5" id="xnX-II-7iN"/>
                         </constraints>
                     </view>
@@ -773,45 +747,5 @@
             </objects>
             <point key="canvasLocation" x="75" y="831"/>
         </scene>
-        <!--SwiftUI View Controller-->
-        <scene sceneID="Z7V-ea-dRX">
-            <objects>
-                <viewController id="ei1-kq-tvV" customClass="SwiftUIViewController" customModule="Kingfisher_macOS_Demo" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" id="JMh-S8-QlI">
-                        <rect key="frame" x="0.0" y="0.0" width="620" height="474"/>
-                        <autoresizingMask key="autoresizingMask"/>
-                    </view>
-                </viewController>
-                <customObject id="BB8-QC-got" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="75" y="1406"/>
-        </scene>
-        <!--Heavy View Controller-->
-        <scene sceneID="kQM-gs-M6P">
-            <objects>
-                <viewController id="19n-lE-hND" customClass="GIFHeavyViewController" customModule="Kingfisher_macOS_Demo" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" id="4Qc-Su-iB9">
-                        <rect key="frame" x="0.0" y="0.0" width="569" height="480"/>
-                        <autoresizingMask key="autoresizingMask"/>
-                        <subviews>
-                            <stackView distribution="fillEqually" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="b04-pQ-0pR">
-                                <rect key="frame" x="0.0" y="0.0" width="569" height="480"/>
-                            </stackView>
-                        </subviews>
-                        <constraints>
-                            <constraint firstAttribute="bottom" secondItem="b04-pQ-0pR" secondAttribute="bottom" id="WP7-Ep-JhF"/>
-                            <constraint firstItem="b04-pQ-0pR" firstAttribute="leading" secondItem="4Qc-Su-iB9" secondAttribute="leading" id="t0Z-wK-nRD"/>
-                            <constraint firstAttribute="trailing" secondItem="b04-pQ-0pR" secondAttribute="trailing" id="yeI-o0-y8O"/>
-                            <constraint firstItem="b04-pQ-0pR" firstAttribute="top" secondItem="4Qc-Su-iB9" secondAttribute="top" id="yhl-hr-4Mw"/>
-                        </constraints>
-                    </view>
-                    <connections>
-                        <outlet property="stackView" destination="b04-pQ-0pR" id="nsI-xM-cZe"/>
-                    </connections>
-                </viewController>
-                <customObject id="Hmf-j6-h1n" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="766.5" y="831"/>
-        </scene>
     </scenes>
 </document>

+ 4 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -120,6 +120,7 @@
 		D8FCF6A821C5A0E500F9ABC0 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */; };
 		D9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */; };
 		DCEB2842257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */; };
+		E9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */; };
 		F72CE9CE1FCF17ED00CC522A /* ImageModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */; };
 /* End PBXBuildFile section */
 
@@ -298,6 +299,7 @@
 		D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedirectHandler.swift; sourceTree = "<group>"; };
 		D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = "<group>"; };
 		DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TVMonogramView+Kingfisher.swift"; sourceTree = "<group>"; };
+		E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HasImageComponent+Kingfisher.swift"; sourceTree = "<group>"; };
 		F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageModifierTests.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -396,6 +398,7 @@
 				D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */,
 				D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */,
 				D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */,
+				E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */,
 				D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */,
 				D12AB6AF215D2BB50013BA68 /* WKInterfaceImage+Kingfisher.swift */,
 				DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */,
@@ -832,6 +835,7 @@
 				D12AB6D4215D2BB50013BA68 /* Image.swift in Sources */,
 				D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */,
 				4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */,
+				E9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */,
 				D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */,
 				D18B3222251852E100662F63 /* KF.swift in Sources */,
 				D12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */,

+ 246 - 0
Sources/Extensions/HasImageComponent+Kingfisher.swift

@@ -0,0 +1,246 @@
+//
+//  KingfisherHasImageComponent+Kingfisher.swift
+//  Kingfisher
+//
+//  Created by JH on 2023/12/5.
+//
+//  Copyright (c) 2023 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.
+
+public protocol KingfisherHasImageComponent: KingfisherCompatible {
+    var image: KFCrossPlatformImage? { set get }
+}
+
+#if canImport(AppKit) && !targetEnvironment(macCatalyst)
+import AppKit
+
+@available(macOS 13.0, *)
+extension NSComboButton: KingfisherHasImageComponent {}
+
+@available(macOS 13.0, *)
+extension NSColorWell: KingfisherHasImageComponent {}
+
+extension NSImageView: KingfisherHasImageComponent {}
+
+extension NSTableViewRowAction: KingfisherHasImageComponent {}
+
+extension NSMenuItem: KingfisherHasImageComponent {}
+
+extension NSPathControlItem: KingfisherHasImageComponent {}
+
+extension NSToolbarItem: KingfisherHasImageComponent {}
+
+extension NSTabViewItem: KingfisherHasImageComponent {}
+
+extension NSStatusItem: KingfisherHasImageComponent {}
+
+extension NSCell: KingfisherHasImageComponent {}
+
+#endif
+
+#if canImport(UIKit) && !os(watchOS)
+import UIKit
+
+@available(iOS 13.0, tvOS 13.0, *)
+extension UIAction: KingfisherHasImageComponent {}
+
+@available(iOS 13.0, tvOS 13.0, *)
+extension UICommand: KingfisherHasImageComponent {}
+
+extension UIBarItem: KingfisherHasImageComponent {}
+
+#endif
+
+extension KingfisherWrapper where Base: KingfisherHasImageComponent {
+    // MARK: Setting Image
+
+    /// Sets an image to the component with a source.
+    ///
+    /// - Parameters:
+    ///   - source: The `Source` object contains information about how to get the image.
+    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.
+    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
+    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
+    ///                    `expectedContentLength`, this block will not be called.
+    ///   - completionHandler: Called when the image retrieved and set finished.
+    /// - Returns: A task represents the image downloading.
+    ///
+    /// - Note:
+    /// Internally, this method will use `KingfisherManager` to get the requested source.
+    /// Since this method will perform UI changes, you must call it from the main thread.
+    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
+    ///
+    @discardableResult
+    public func setImage(
+        with source: Source?,
+        placeholder: KFCrossPlatformImage? = nil,
+        options: KingfisherOptionsInfo? = nil,
+        progressBlock: DownloadProgressBlock? = nil,
+        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil
+    ) -> DownloadTask? {
+        let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
+        return setImage(
+            with: source,
+            placeholder: placeholder,
+            parsedOptions: options,
+            progressBlock: progressBlock,
+            completionHandler: completionHandler
+        )
+    }
+
+    /// Sets an image to the component with a requested resource.
+    ///
+    /// - Parameters:
+    ///   - resource: The `Resource` object contains information about the resource.
+    ///   - placeholder: A placeholder to show while retrieving the image from the given `resource`.
+    ///   - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
+    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
+    ///                    `expectedContentLength`, this block will not be called.
+    ///   - completionHandler: Called when the image retrieved and set finished.
+    /// - Returns: A task represents the image downloading.
+    ///
+    /// - Note:
+    /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
+    /// or network. Since this method will perform UI changes, you must call it from the main thread.
+    /// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
+    ///
+    @discardableResult
+    public func setImage(
+        with resource: Resource?,
+        placeholder: KFCrossPlatformImage? = nil,
+        options: KingfisherOptionsInfo? = nil,
+        progressBlock: DownloadProgressBlock? = nil,
+        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil
+    ) -> DownloadTask? {
+        return setImage(
+            with: resource?.convertToSource(),
+            placeholder: placeholder,
+            options: options,
+            progressBlock: progressBlock,
+            completionHandler: completionHandler
+        )
+    }
+
+    func setImage(
+        with source: Source?,
+        placeholder: KFCrossPlatformImage? = nil,
+        parsedOptions: KingfisherParsedOptionsInfo,
+        progressBlock: DownloadProgressBlock? = nil,
+        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil
+    ) -> DownloadTask? {
+        var mutatingSelf = self
+        guard let source else {
+            base.image = placeholder
+            mutatingSelf.taskIdentifier = nil
+            completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
+            return nil
+        }
+
+        var options = parsedOptions
+        if !options.keepCurrentImageWhileLoading {
+            base.image = placeholder
+        }
+
+        let issuedIdentifier = Source.Identifier.next()
+        mutatingSelf.taskIdentifier = issuedIdentifier
+
+        if let block = progressBlock {
+            options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
+        }
+
+        let task = KingfisherManager.shared.retrieveImage(
+            with: source,
+            options: options,
+            downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
+            progressiveImageSetter: { self.base.image = $0 },
+            referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
+            completionHandler: { result in
+                CallbackQueue.mainCurrentOrAsync.execute {
+                    guard issuedIdentifier == self.taskIdentifier else {
+                        let reason: KingfisherError.ImageSettingErrorReason
+                        do {
+                            let value = try result.get()
+                            reason = .notCurrentSourceTask(result: value, error: nil, source: source)
+                        } catch {
+                            reason = .notCurrentSourceTask(result: nil, error: error, source: source)
+                        }
+                        let error = KingfisherError.imageSettingError(reason: reason)
+                        completionHandler?(.failure(error))
+                        return
+                    }
+
+                    mutatingSelf.imageTask = nil
+                    mutatingSelf.taskIdentifier = nil
+
+                    switch result {
+                    case let .success(value):
+                        self.base.image = value.image
+                        completionHandler?(result)
+
+                    case .failure:
+                        if let image = options.onFailureImage {
+                            self.base.image = image
+                        }
+                        completionHandler?(result)
+                    }
+                }
+            }
+        )
+
+        mutatingSelf.imageTask = task
+        return task
+    }
+
+    // MARK: Cancelling Downloading Task
+
+    /// Cancels the image download task of the component if it is running.
+    /// Nothing will happen if the downloading has already finished.
+    public func cancelImageDownloadTask() {
+        imageTask?.cancel()
+    }
+}
+
+// MARK: - Associated Object
+
+private var taskIdentifierKey: Void?
+private var imageTaskKey: Void?
+
+private var alternateTaskIdentifierKey: Void?
+private var alternateImageTaskKey: Void?
+
+extension KingfisherWrapper where Base: KingfisherHasImageComponent {
+    // MARK: Properties
+
+    public private(set) var taskIdentifier: Source.Identifier.Value? {
+        get {
+            let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
+            return box?.value
+        }
+        set {
+            let box = newValue.map { Box($0) }
+            setRetainedAssociatedObject(base, &taskIdentifierKey, box)
+        }
+    }
+
+    private var imageTask: DownloadTask? {
+        get { return getAssociatedObject(base, &imageTaskKey) }
+        set { setRetainedAssociatedObject(base, &imageTaskKey, newValue) }
+    }
+}