From cf2618d7ab3c581e26c55d4cdfd9c5a4fa5b06f1 Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Tue, 5 Dec 2023 22:16:01 +0800 Subject: [PATCH 01/24] Add extension to has image property components --- Kingfisher.xcodeproj/project.pbxproj | 4 + Sources/Extensions/AppKit+Kingfisher.swift | 229 +++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 Sources/Extensions/AppKit+Kingfisher.swift diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index 3257bf6eb..263ce4ba9 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/Kingfisher.xcodeproj/project.pbxproj @@ -119,6 +119,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 /* AppKit+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E3ED8A2B1F66B200734CFF /* AppKit+Kingfisher.swift */; }; F72CE9CE1FCF17ED00CC522A /* ImageModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */; }; /* End PBXBuildFile section */ @@ -295,6 +296,7 @@ D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedirectHandler.swift; sourceTree = ""; }; D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = ""; }; DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; + E9E3ED8A2B1F66B200734CFF /* AppKit+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppKit+Kingfisher.swift"; sourceTree = ""; }; F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageModifierTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -392,6 +394,7 @@ D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */, D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */, D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */, + E9E3ED8A2B1F66B200734CFF /* AppKit+Kingfisher.swift */, D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */, D12AB6AF215D2BB50013BA68 /* WKInterfaceImage+Kingfisher.swift */, DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */, @@ -827,6 +830,7 @@ D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */, D12AB728215D2BB50013BA68 /* String+MD5.swift in Sources */, 4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */, + E9E3ED8B2B1F66B200734CFF /* AppKit+Kingfisher.swift in Sources */, D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */, D18B3222251852E100662F63 /* KF.swift in Sources */, D12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */, diff --git a/Sources/Extensions/AppKit+Kingfisher.swift b/Sources/Extensions/AppKit+Kingfisher.swift new file mode 100644 index 000000000..b04b53e96 --- /dev/null +++ b/Sources/Extensions/AppKit+Kingfisher.swift @@ -0,0 +1,229 @@ +// +// AppKit+Kingfisher.swift +// Kingfisher +// +// Created by JH on 2023/12/5. +// +// Copyright (c) 2023 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 canImport(AppKit) && !targetEnvironment(macCatalyst) + +public protocol KingfisherHasImageComponent: AnyObject { + var image: NSImage? { set get } +} + + +import AppKit + +extension KingfisherWrapper where Base: KingfisherHasImageComponent { + + // MARK: Setting Image + + /// Sets an image to the button 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) -> 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 button 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) -> 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) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = 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 .success(let 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 button 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? = 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)} + } + + public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) + } + } + + private var alternateImageTask: DownloadTask? { + get { return getAssociatedObject(base, &alternateImageTaskKey) } + set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} + } +} +#endif From 9cf6e07e856a89c5000477d602f7f83e9f45bc25 Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Tue, 5 Dec 2023 22:18:34 +0800 Subject: [PATCH 02/24] Add some extensions --- Sources/Extensions/AppKit+Kingfisher.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Extensions/AppKit+Kingfisher.swift b/Sources/Extensions/AppKit+Kingfisher.swift index b04b53e96..876dcd2f1 100644 --- a/Sources/Extensions/AppKit+Kingfisher.swift +++ b/Sources/Extensions/AppKit+Kingfisher.swift @@ -30,9 +30,13 @@ public protocol KingfisherHasImageComponent: AnyObject { var image: NSImage? { set get } } - import AppKit +@available(macOS 13.0, *) +extension NSComboButton: KingfisherHasImageComponent {} + +extension NSMenuItem: KingfisherHasImageComponent {} + extension KingfisherWrapper where Base: KingfisherHasImageComponent { // MARK: Setting Image From fa98a1f2e4afcc1427ffb7cbf0d11d553bcc034b Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Tue, 5 Dec 2023 23:06:01 +0800 Subject: [PATCH 03/24] Add some extensions --- Sources/Extensions/AppKit+Kingfisher.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Sources/Extensions/AppKit+Kingfisher.swift b/Sources/Extensions/AppKit+Kingfisher.swift index 876dcd2f1..bbaef6dee 100644 --- a/Sources/Extensions/AppKit+Kingfisher.swift +++ b/Sources/Extensions/AppKit+Kingfisher.swift @@ -26,7 +26,7 @@ #if canImport(AppKit) && !targetEnvironment(macCatalyst) -public protocol KingfisherHasImageComponent: AnyObject { +public protocol KingfisherHasImageComponent: KingfisherCompatible { var image: NSImage? { set get } } @@ -35,8 +35,25 @@ 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 {} + extension KingfisherWrapper where Base: KingfisherHasImageComponent { // MARK: Setting Image From 2113a6287221fe186425fad5594c22a7256665d5 Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:07:07 +0800 Subject: [PATCH 04/24] Adaptation to multiple platforms --- Kingfisher.xcodeproj/project.pbxproj | 8 +-- ...ift => HasImageComponent+Kingfisher.swift} | 59 +++++++------------ 2 files changed, 25 insertions(+), 42 deletions(-) rename Sources/Extensions/{AppKit+Kingfisher.swift => HasImageComponent+Kingfisher.swift} (88%) diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index 263ce4ba9..eb950c1cf 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/Kingfisher.xcodeproj/project.pbxproj @@ -119,7 +119,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 /* AppKit+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E3ED8A2B1F66B200734CFF /* AppKit+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 */ @@ -296,7 +296,7 @@ D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedirectHandler.swift; sourceTree = ""; }; D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = ""; }; DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; - E9E3ED8A2B1F66B200734CFF /* AppKit+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppKit+Kingfisher.swift"; sourceTree = ""; }; + E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HasImageComponent+Kingfisher.swift"; sourceTree = ""; }; F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageModifierTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -394,7 +394,7 @@ D12EB83B24DD8EFC00329EE1 /* NSTextAttachment+Kingfisher.swift */, D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */, D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */, - E9E3ED8A2B1F66B200734CFF /* AppKit+Kingfisher.swift */, + E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */, D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */, D12AB6AF215D2BB50013BA68 /* WKInterfaceImage+Kingfisher.swift */, DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */, @@ -830,7 +830,7 @@ D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */, D12AB728215D2BB50013BA68 /* String+MD5.swift in Sources */, 4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */, - E9E3ED8B2B1F66B200734CFF /* AppKit+Kingfisher.swift in Sources */, + E9E3ED8B2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift in Sources */, D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */, D18B3222251852E100662F63 /* KF.swift in Sources */, D12AB704215D2BB50013BA68 /* Kingfisher.swift in Sources */, diff --git a/Sources/Extensions/AppKit+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift similarity index 88% rename from Sources/Extensions/AppKit+Kingfisher.swift rename to Sources/Extensions/HasImageComponent+Kingfisher.swift index bbaef6dee..41c9d3681 100644 --- a/Sources/Extensions/AppKit+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -24,12 +24,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#if canImport(AppKit) && !targetEnvironment(macCatalyst) - public protocol KingfisherHasImageComponent: KingfisherCompatible { - var image: NSImage? { set get } + var image: KFCrossPlatformImage? { set get } } +#if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit @available(macOS 13.0, *) @@ -54,11 +53,12 @@ extension NSStatusItem: KingfisherHasImageComponent {} extension NSCell: KingfisherHasImageComponent {} -extension KingfisherWrapper where Base: KingfisherHasImageComponent { +#endif +extension KingfisherWrapper where Base: KingfisherHasImageComponent { // MARK: Setting Image - /// Sets an image to the button with a source. + /// Sets an image to the component with a source. /// /// - Parameters: /// - source: The `Source` object contains information about how to get the image. @@ -80,8 +80,8 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, - completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? - { + completionHandler: ((Result) -> Void)? = nil + ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( with: source, @@ -92,7 +92,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { ) } - /// Sets an image to the button with a requested resource. + /// Sets an image to the component with a requested resource. /// /// - Parameters: /// - resource: The `Resource` object contains information about the resource. @@ -114,14 +114,15 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, - completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? - { + completionHandler: ((Result) -> Void)? = nil + ) -> DownloadTask? { return setImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, - completionHandler: completionHandler) + completionHandler: completionHandler + ) } func setImage( @@ -129,10 +130,10 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, - completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? - { + completionHandler: ((Result) -> Void)? = nil + ) -> DownloadTask? { var mutatingSelf = self - guard let source = source else { + guard let source else { base.image = placeholder mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) @@ -176,7 +177,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { mutatingSelf.taskIdentifier = nil switch result { - case .success(let value): + case let .success(value): self.base.image = value.image completionHandler?(result) @@ -196,15 +197,15 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { // MARK: Cancelling Downloading Task - /// Cancels the image download task of the button if it is running. + /// 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? @@ -212,9 +213,8 @@ 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? = getAssociatedObject(base, &taskIdentifierKey) @@ -225,26 +225,9 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } - + private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } - set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} - } - - public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { - get { - let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) - return box?.value - } - set { - let box = newValue.map { Box($0) } - setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) - } - } - - private var alternateImageTask: DownloadTask? { - get { return getAssociatedObject(base, &alternateImageTaskKey) } - set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue) } } } -#endif From dcd5693a0298922bc019782ef411be5f80dcee6c Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:28:53 +0800 Subject: [PATCH 05/24] Create or update Upstream Sync workflow file. --- .github/workflows/UpstreamSync.yml | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/UpstreamSync.yml diff --git a/.github/workflows/UpstreamSync.yml b/.github/workflows/UpstreamSync.yml new file mode 100644 index 000000000..5df5b1f79 --- /dev/null +++ b/.github/workflows/UpstreamSync.yml @@ -0,0 +1,39 @@ +name: Upstream Sync + +permissions: + contents: write + +on: + schedule: + - cron: "0 0 * * *" # every day + workflow_dispatch: + +jobs: + sync_latest_from_upstream: + name: Sync latest commits from upstream repo + runs-on: ubuntu-latest + if: ${{ github.event.repository.fork }} + + steps: + # Step 1: run a standard checkout action + - name: Checkout target repo + uses: actions/checkout@v3 + + # Step 2: run the sync action + - name: Sync upstream changes + id: sync + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + with: + upstream_sync_repo: onevcat/Kingfisher + upstream_sync_branch: master + target_sync_branch: master + target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set + + # Set test_mode true to run tests instead of the true action!! + test_mode: false + + - name: Sync check + if: failure() + run: | + echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." + exit 1 \ No newline at end of file From 86baa0109fa945b860e6c3b39fc69e7228b26bf2 Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Wed, 1 May 2024 00:00:23 +0800 Subject: [PATCH 06/24] Conflict resolution --- .../Base.lproj/Main.storyboard | 74 +------------------ 1 file changed, 4 insertions(+), 70 deletions(-) diff --git a/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard b/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard index 1dc60a2a2..e11aa8ffe 100644 --- a/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard @@ -1,7 +1,6 @@ - @@ -670,10 +669,10 @@ - + - + @@ -682,7 +681,7 @@ - + @@ -717,16 +716,6 @@ - - - - + - - - - @@ -773,45 +747,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 38900fd172a159c2f094335ba4fafc75f7c3c83f Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Wed, 1 May 2024 00:31:23 +0800 Subject: [PATCH 07/24] Add UIKit Components extensions --- .github/workflows/UpstreamSync.yml | 39 ------------------- .../HasImageComponent+Kingfisher.swift | 15 ++++++- 2 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 .github/workflows/UpstreamSync.yml diff --git a/.github/workflows/UpstreamSync.yml b/.github/workflows/UpstreamSync.yml deleted file mode 100644 index 5df5b1f79..000000000 --- a/.github/workflows/UpstreamSync.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Upstream Sync - -permissions: - contents: write - -on: - schedule: - - cron: "0 0 * * *" # every day - workflow_dispatch: - -jobs: - sync_latest_from_upstream: - name: Sync latest commits from upstream repo - runs-on: ubuntu-latest - if: ${{ github.event.repository.fork }} - - steps: - # Step 1: run a standard checkout action - - name: Checkout target repo - uses: actions/checkout@v3 - - # Step 2: run the sync action - - name: Sync upstream changes - id: sync - uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 - with: - upstream_sync_repo: onevcat/Kingfisher - upstream_sync_branch: master - target_sync_branch: master - target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set - - # Set test_mode true to run tests instead of the true action!! - test_mode: false - - - name: Sync check - if: failure() - run: | - echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." - exit 1 \ No newline at end of file diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 41c9d3681..956390297 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -1,5 +1,5 @@ // -// AppKit+Kingfisher.swift +// KingfisherHasImageComponent+Kingfisher.swift // Kingfisher // // Created by JH on 2023/12/5. @@ -55,6 +55,19 @@ extension NSCell: KingfisherHasImageComponent {} #endif +#if canImport(UIKit) +import UIKit + +@available(iOS 13.0, *) +extension UIAction: KingfisherHasImageComponent {} + +@available(iOS 13.0, *) +extension UICommand: KingfisherHasImageComponent {} + +extension UIBarItem: KingfisherHasImageComponent {} + +#endif + extension KingfisherWrapper where Base: KingfisherHasImageComponent { // MARK: Setting Image From ca93992e7b9004a9c6eab583f9fdfd7cad0b7040 Mon Sep 17 00:00:00 2001 From: Mx-Iris <61279231+Mx-Iris@users.noreply.github.com> Date: Wed, 1 May 2024 00:40:53 +0800 Subject: [PATCH 08/24] Fixes compatibility issues with other platforms --- Sources/Extensions/HasImageComponent+Kingfisher.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 956390297..46d0dde36 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -55,13 +55,13 @@ extension NSCell: KingfisherHasImageComponent {} #endif -#if canImport(UIKit) +#if canImport(UIKit) && !os(watchOS) import UIKit -@available(iOS 13.0, *) +@available(iOS 13.0, tvOS 13.0, *) extension UIAction: KingfisherHasImageComponent {} -@available(iOS 13.0, *) +@available(iOS 13.0, tvOS 13.0, *) extension UICommand: KingfisherHasImageComponent {} extension UIBarItem: KingfisherHasImageComponent {} From 64589305e55f702b3b8ce60bfb82b6a545612b9b Mon Sep 17 00:00:00 2001 From: onevcat Date: Fri, 3 May 2024 17:59:48 +0900 Subject: [PATCH 09/24] Reset storyboard --- .../Base.lproj/Main.storyboard | 74 ++++++++++++++++++- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard b/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard index e11aa8ffe..1dc60a2a2 100644 --- a/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard +++ b/Demo/Demo/Kingfisher-macOS-Demo/Base.lproj/Main.storyboard @@ -1,6 +1,7 @@ + @@ -669,10 +670,10 @@ - + - + @@ -681,7 +682,7 @@ - + @@ -716,6 +717,16 @@ + + + - + + + + + @@ -747,5 +773,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2f1cce34a5f97400128d3d4bae4add4915e92bb9 Mon Sep 17 00:00:00 2001 From: onevcat Date: Fri, 3 May 2024 22:47:01 +0900 Subject: [PATCH 10/24] Sync changes in v8 to HasImageComponent --- .../HasImageComponent+Kingfisher.swift | 315 +++++++++++++++--- Sources/Image/Placeholder.swift | 9 + 2 files changed, 269 insertions(+), 55 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 46d0dde36..ede1a79ab 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -25,7 +25,7 @@ // THE SOFTWARE. public protocol KingfisherHasImageComponent: KingfisherCompatible { - var image: KFCrossPlatformImage? { set get } + @MainActor var image: KFCrossPlatformImage? { set get } } #if canImport(AppKit) && !targetEnvironment(macCatalyst) @@ -37,8 +37,6 @@ extension NSComboButton: KingfisherHasImageComponent {} @available(macOS 13.0, *) extension NSColorWell: KingfisherHasImageComponent {} -extension NSImageView: KingfisherHasImageComponent {} - extension NSTableViewRowAction: KingfisherHasImageComponent {} extension NSMenuItem: KingfisherHasImageComponent {} @@ -68,72 +66,250 @@ extension UIBarItem: KingfisherHasImageComponent {} #endif +#if !os(watchOS) +@MainActor extension KingfisherWrapper where Base: KingfisherHasImageComponent { + // MARK: Setting Image - /// Sets an image to the component with a source. + /// Sets an image to the image view 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 + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is 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. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension + /// methods. So the code above is equivalent to: /// - /// - 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. + /// ```swift + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with source: Source?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, - completionHandler: ((Result) -> Void)? = nil - ) -> DownloadTask? { + completionHandler: (@MainActor @Sendable (Result) -> 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 image view with a ``Source``. + /// + /// - Parameters: + /// - source: The ``Source`` object that defines data information from the network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both ``Source/network(_:)`` and ``Source/provider(_:)`` sources, there are corresponding view extension + /// methods. So the code above is equivalent to: + /// + /// ```swift + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { return setImage( with: source, placeholder: placeholder, - parsedOptions: options, - progressBlock: progressBlock, + options: options, + progressBlock: nil, completionHandler: completionHandler ) } - - /// Sets an image to the component with a requested resource. + + /// Sets an image to the image view 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 + /// - resource: The ``Resource`` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is 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. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: /// - /// - 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. + /// ```swift + /// // Set image from a URL resource. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. @discardableResult public func setImage( with resource: Resource?, placeholder: KFCrossPlatformImage? = nil, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, - completionHandler: ((Result) -> Void)? = nil - ) -> DownloadTask? { + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { return setImage( with: resource?.convertToSource(), placeholder: placeholder, options: options, progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view 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 `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ```swift + /// // Set image from a URL resource. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - progressBlock: Called when the image downloading progress is updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a ``ImageDataProvider``. + /// + /// - Parameters: + /// - provider: The ``ImageDataProvider`` object that defines data information from the data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `source`. + /// - options: A set of options to define image setting behaviors. See ``KingfisherOptionsInfo`` for more. + /// - completionHandler: Called when the image retrieval and setting are finished. + /// - Returns: A task that represents the image downloading. + /// + /// Internally, this method will use ``KingfisherManager`` to get the source. Since this method will perform UI + /// changes, it is your responsibility to call it from the main thread. + /// + /// > Both `progressBlock` and `completionHandler` will also be executed in the main thread. + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { + return setImage( + with: provider, + placeholder: placeholder, + options: options, + progressBlock: nil, completionHandler: completionHandler ) } @@ -143,19 +319,22 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, - completionHandler: ((Result) -> Void)? = nil - ) -> DownloadTask? { + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? + { var mutatingSelf = self - guard let source else { - base.image = placeholder + guard let source = source else { mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } var options = parsedOptions - if !options.keepCurrentImageWhileLoading { - base.image = placeholder + + let isEmptyImage = base.image == nil && self.placeholder == nil + if !options.keepCurrentImageWhileLoading || isEmptyImage { + // Always set placeholder while there is no image/placeholder yet. + mutatingSelf.placeholder = placeholder } let issuedIdentifier = Source.Identifier.next() @@ -168,11 +347,13 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { let task = KingfisherManager.shared.retrieveImage( with: source, options: options, - downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + downloadTaskUpdated: { task in + Task { @MainActor in mutatingSelf.imageTask = task } + }, progressiveImageSetter: { self.base.image = $0 }, referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, completionHandler: { result in - CallbackQueue.mainCurrentOrAsync.execute { + CallbackQueueMain.currentOrAsync { guard issuedIdentifier == self.taskIdentifier else { let reason: KingfisherError.ImageSettingErrorReason do { @@ -190,12 +371,15 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { mutatingSelf.taskIdentifier = nil switch result { - case let .success(value): - self.base.image = value.image - completionHandler?(result) + case .success(let value): + + mutatingSelf.placeholder = nil + self.base.image = value.image + completionHandler?(result) case .failure: if let image = options.onFailureImage { + mutatingSelf.placeholder = nil self.base.image = image } completionHandler?(result) @@ -203,31 +387,31 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { } } ) - mutatingSelf.imageTask = task return task } // MARK: Cancelling Downloading Task - /// Cancels the image download task of the component if it is running. + /// Cancels the image download task of the image view if it is running. + /// /// Nothing will happen if the downloading has already finished. - public func cancelImageDownloadTask() { + public func cancelDownloadTask() { imageTask?.cancel() } } // MARK: - Associated Object +@MainActor private var taskIdentifierKey: Void? +@MainActor private var indicatorKey: Void? +@MainActor private var indicatorTypeKey: Void? +@MainActor private var placeholderKey: Void? +@MainActor private var imageTaskKey: Void? -private var taskIdentifierKey: Void? -private var imageTaskKey: Void? - -private var alternateTaskIdentifierKey: Void? -private var alternateImageTaskKey: Void? - +@MainActor extension KingfisherWrapper where Base: KingfisherHasImageComponent { - // MARK: Properties + // MARK: Properties public private(set) var taskIdentifier: Source.Identifier.Value? { get { let box: Box? = getAssociatedObject(base, &taskIdentifierKey) @@ -238,9 +422,30 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { setRetainedAssociatedObject(base, &taskIdentifierKey, box) } } - + private var imageTask: DownloadTask? { get { return getAssociatedObject(base, &imageTaskKey) } - set { setRetainedAssociatedObject(base, &imageTaskKey, newValue) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + /// Represents the ``Placeholder`` used for this image view. + /// + /// A ``Placeholder`` will be shown in the view while it is downloading an image. + public private(set) var placeholder: KFCrossPlatformImage? { + get { return getAssociatedObject(base, &placeholderKey) } + set { + if let previousPlaceholder = placeholder { + previousPlaceholder.remove(from: base) + } + + if let newPlaceholder = newValue { + newPlaceholder.add(to: base) + } else { + base.image = nil + } + setRetainedAssociatedObject(base, &placeholderKey, newValue) + } } } + +#endif diff --git a/Sources/Image/Placeholder.swift b/Sources/Image/Placeholder.swift index 9202e152a..f45862f56 100644 --- a/Sources/Image/Placeholder.swift +++ b/Sources/Image/Placeholder.swift @@ -55,6 +55,7 @@ public protocol Placeholder { @MainActor func remove(from imageView: KFCrossPlatformImageView) } +@MainActor extension KFCrossPlatformImage: Placeholder { public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self @@ -63,6 +64,14 @@ extension KFCrossPlatformImage: Placeholder { public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil } + + public func add(to base: any KingfisherHasImageComponent) { + base.image = self + } + + public func remove(from base: any KingfisherHasImageComponent) { + base.image = nil + } } /// Default implementation of an arbitrary view as a placeholder. The view will be From 362b543e7bc78bd332e6b87200c128cb510108f2 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 5 May 2024 06:36:04 +0900 Subject: [PATCH 11/24] Remove placeholder and indicator things from KingfisherHasImageComponent --- .../HasImageComponent+Kingfisher.swift | 34 +++---------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index ede1a79ab..7b9f0b31f 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -331,10 +331,9 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { var options = parsedOptions - let isEmptyImage = base.image == nil && self.placeholder == nil - if !options.keepCurrentImageWhileLoading || isEmptyImage { + if !options.keepCurrentImageWhileLoading || base.image == nil { // Always set placeholder while there is no image/placeholder yet. - mutatingSelf.placeholder = placeholder + mutatingSelf.base.image = placeholder } let issuedIdentifier = Source.Identifier.next() @@ -372,14 +371,11 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { switch result { case .success(let value): - - mutatingSelf.placeholder = nil - self.base.image = value.image - completionHandler?(result) + self.base.image = value.image + completionHandler?(result) case .failure: if let image = options.onFailureImage { - mutatingSelf.placeholder = nil self.base.image = image } completionHandler?(result) @@ -403,9 +399,6 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { // MARK: - Associated Object @MainActor private var taskIdentifierKey: Void? -@MainActor private var indicatorKey: Void? -@MainActor private var indicatorTypeKey: Void? -@MainActor private var placeholderKey: Void? @MainActor private var imageTaskKey: Void? @MainActor @@ -427,25 +420,6 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } - - /// Represents the ``Placeholder`` used for this image view. - /// - /// A ``Placeholder`` will be shown in the view while it is downloading an image. - public private(set) var placeholder: KFCrossPlatformImage? { - get { return getAssociatedObject(base, &placeholderKey) } - set { - if let previousPlaceholder = placeholder { - previousPlaceholder.remove(from: base) - } - - if let newPlaceholder = newValue { - newPlaceholder.add(to: base) - } else { - base.image = nil - } - setRetainedAssociatedObject(base, &placeholderKey, newValue) - } - } } #endif From 5d2bea8c6d3aa29cc73920c3e267d95ebe93774b Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 5 May 2024 06:50:01 +0900 Subject: [PATCH 12/24] Adjust format and minor refactor --- .../HasImageComponent+Kingfisher.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 7b9f0b31f..a92f0d0eb 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -118,7 +118,13 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { ) -> DownloadTask? { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) - return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) } /// Sets an image to the image view with a ``Source``. @@ -210,7 +216,8 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { placeholder: placeholder, options: options, progressBlock: progressBlock, - completionHandler: completionHandler) + completionHandler: completionHandler + ) } /// Sets an image to the image view with a requested ``Resource``. @@ -281,7 +288,8 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { placeholder: placeholder, options: options, progressBlock: progressBlock, - completionHandler: completionHandler) + completionHandler: completionHandler + ) } /// Sets an image to the image view with a ``ImageDataProvider``. @@ -324,6 +332,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { { var mutatingSelf = self guard let source = source else { + base.image = placeholder mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil @@ -372,14 +381,12 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { switch result { case .success(let value): self.base.image = value.image - completionHandler?(result) - case .failure: if let image = options.onFailureImage { self.base.image = image } - completionHandler?(result) } + completionHandler?(result) } } ) From 4298e26ecd2a4a4a1d1b5161c9b896535150d300 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 5 May 2024 07:06:32 +0900 Subject: [PATCH 13/24] Add watchOS support in KingfisherHasImageComponent --- .../HasImageComponent+Kingfisher.swift | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index a92f0d0eb..6f70af160 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -50,7 +50,6 @@ extension NSTabViewItem: KingfisherHasImageComponent {} extension NSStatusItem: KingfisherHasImageComponent {} extension NSCell: KingfisherHasImageComponent {} - #endif #if canImport(UIKit) && !os(watchOS) @@ -66,7 +65,16 @@ extension UIBarItem: KingfisherHasImageComponent {} #endif -#if !os(watchOS) +#if canImport(WatchKit) +import WatchKit +extension WKInterfaceImage: KingfisherHasImageComponent { + @MainActor public var image: KFCrossPlatformImage? { + get { nil } + set { setImage(newValue) } + } +} +#endif + @MainActor extension KingfisherWrapper where Base: KingfisherHasImageComponent { @@ -340,9 +348,14 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { var options = parsedOptions - if !options.keepCurrentImageWhileLoading || base.image == nil { - // Always set placeholder while there is no image/placeholder yet. - mutatingSelf.base.image = placeholder + // Always set placeholder while there is no image/placeholder yet. +#if os(watchOS) + let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading +#else + let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading || base.image == nil +#endif + if usePlaceholderDuringLoading { + base.image = placeholder } let issuedIdentifier = Source.Identifier.next() @@ -428,5 +441,3 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } } - -#endif From 26a3005efc11da32d3231a66140e7b6946ff852d Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 5 May 2024 07:08:45 +0900 Subject: [PATCH 14/24] Remove watchOS specified interface Since now we can use KingfisherHasImageComponent to replace it --- Kingfisher.xcodeproj/project.pbxproj | 4 - .../WKInterfaceImage+Kingfisher.swift | 208 ------------------ 2 files changed, 212 deletions(-) delete mode 100644 Sources/Extensions/WKInterfaceImage+Kingfisher.swift diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index de181e2d7..75c0f1dca 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/Kingfisher.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; }; 4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; }; 4BE688F722FD513100B11168 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */; }; - 4BE688F822FD513700B11168 /* WKInterfaceImage+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6AF215D2BB50013BA68 /* WKInterfaceImage+Kingfisher.swift */; }; 76FB4FD2262D773E006D15F8 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FB4FD1262D773E006D15F8 /* GraphicsContext.swift */; }; C9286407228584EB00257182 /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9286406228584EB00257182 /* ImageProgressive.swift */; }; D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1132C9625919F69003E528D /* KFOptionsSetter.swift */; }; @@ -191,7 +190,6 @@ D12AB6AC215D2BB50013BA68 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ImageView+Kingfisher.swift"; sourceTree = ""; }; D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSButton+Kingfisher.swift"; sourceTree = ""; }; D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIButton+Kingfisher.swift"; sourceTree = ""; }; - D12AB6AF215D2BB50013BA68 /* WKInterfaceImage+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WKInterfaceImage+Kingfisher.swift"; sourceTree = ""; }; D12AB6B1215D2BB50013BA68 /* Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Kingfisher.swift; sourceTree = ""; }; D12AB6B2215D2BB50013BA68 /* KingfisherError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherError.swift; sourceTree = ""; }; D12AB6B3215D2BB50013BA68 /* KingfisherManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KingfisherManager.swift; sourceTree = ""; }; @@ -400,7 +398,6 @@ D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */, E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */, D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */, - D12AB6AF215D2BB50013BA68 /* WKInterfaceImage+Kingfisher.swift */, DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */, 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */, ); @@ -873,7 +870,6 @@ D1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */, 4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */, D12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */, - 4BE688F822FD513700B11168 /* WKInterfaceImage+Kingfisher.swift in Sources */, D12AB724215D2BB50013BA68 /* Box.swift in Sources */, 4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */, 3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */, diff --git a/Sources/Extensions/WKInterfaceImage+Kingfisher.swift b/Sources/Extensions/WKInterfaceImage+Kingfisher.swift deleted file mode 100644 index 5a6f7f1bf..000000000 --- a/Sources/Extensions/WKInterfaceImage+Kingfisher.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// WKInterfaceImage+Kingfisher.swift -// Kingfisher -// -// Created by Rodrigo Borges Soares on 04/05/18. -// -// 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 -// 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 canImport(WatchKit) - -import WatchKit - -@MainActor -extension KingfisherWrapper where Base: WKInterfaceImage { - - // MARK: Setting Image - - /// Sets an image to the image view with a source. - /// - /// - Parameters: - /// - source: The `Source` object contains information about 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: (@Sendable (Result) -> 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 image view with a requested resource. - /// - /// - Parameters: - /// - resource: The `Resource` object contains information about 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 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: (@Sendable (Result) -> 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: (@Sendable (Result) -> Void)? = nil) -> DownloadTask? - { - var mutatingSelf = self - guard let source = source else { - base.setImage(placeholder) - mutatingSelf.taskIdentifier = nil - completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) - return nil - } - - var options = parsedOptions - if !options.keepCurrentImageWhileLoading { - base.setImage(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: { task in - Task { @MainActor in mutatingSelf.imageTask = task } - }, - progressiveImageSetter: { self.base.setImage($0) }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - 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 .success(let value): - self.base.setImage(value.image) - completionHandler?(result) - - case .failure: - if let image = options.onFailureImage { - self.base.setImage(image) - } - completionHandler?(result) - } - } - } - ) - - mutatingSelf.imageTask = task - return task - } - - // MARK: Cancelling Image - - /// Cancel the image download task bounded to the image view if it is running. - /// Nothing will happen if the downloading has already finished. - public func cancelDownloadTask() { - imageTask?.cancel() - } -} - -@MainActor private var taskIdentifierKey: Void? -@MainActor private var imageTaskKey: Void? - -// MARK: Properties -@MainActor -extension KingfisherWrapper where Base: WKInterfaceImage { - - public private(set) var taskIdentifier: Source.Identifier.Value? { - get { - let box: Box? = 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)} - } -} -#endif From 98b17d732846f1fc8204d946695acc6c26193e15 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 5 May 2024 07:48:47 +0900 Subject: [PATCH 15/24] Fix a potential stub issue --- .../ImageViewExtensionTests.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/KingfisherTests/ImageViewExtensionTests.swift b/Tests/KingfisherTests/ImageViewExtensionTests.swift index fff3b21da..04d99de4a 100644 --- a/Tests/KingfisherTests/ImageViewExtensionTests.swift +++ b/Tests/KingfisherTests/ImageViewExtensionTests.swift @@ -420,13 +420,20 @@ class ImageViewExtensionTests: XCTestCase { @MainActor func testSettingImageKeepingRespectingPlaceholder() { let exp = expectation(description: #function) - let url = testURLs[0] - stub(url, data: testImageData) - + // While current image is nil, set placeholder - imageView.kf.setImage(with: url, placeholder: testImage, options: [.keepCurrentImageWhileLoading]) { result in } - XCTAssertNotNil(imageView.image) + let url = testURLs[0] + imageView.kf.setImage(with: url, placeholder: testImage, options: [.keepCurrentImageWhileLoading]) { result in + exp.fulfill() + } XCTAssertEqual(testImage, imageView.image) + waitForExpectations(timeout: 3, handler: nil) + } + + @MainActor func testMe() { + let exp = expectation(description: #function) + let url = testURLs[0] + stub(url, data: testImageData) // While current image is not nil, keep it let anotherImage = KFCrossPlatformImage(data: testImageJEPGData) @@ -435,7 +442,6 @@ class ImageViewExtensionTests: XCTestCase { XCTAssertNotEqual(self.imageView.image, anotherImage) exp.fulfill() } - XCTAssertNotNil(imageView.image) XCTAssertEqual(anotherImage, imageView.image) waitForExpectations(timeout: 3, handler: nil) From ad5d1b73718adda39204efc2f9d7684c1c780f5f Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 5 May 2024 17:50:18 +0900 Subject: [PATCH 16/24] Move TVMonogramView support to extension --- Kingfisher.xcodeproj/project.pbxproj | 4 - .../HasImageComponent+Kingfisher.swift | 18 +- .../TVMonogramView+Kingfisher.swift | 216 ------------------ 3 files changed, 5 insertions(+), 233 deletions(-) delete mode 100644 Sources/Extensions/TVMonogramView+Kingfisher.swift diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index 75c0f1dca..f354adfc5 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/Kingfisher.xcodeproj/project.pbxproj @@ -118,7 +118,6 @@ D1F1F6FF24625EC600910725 /* RetryStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F1F6FE24625EC600910725 /* RetryStrategyTests.swift */; }; 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 */ @@ -296,7 +295,6 @@ D1F7607623097532000C5269 /* KFImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KFImage.swift; sourceTree = ""; }; D8FCF6A721C5A0E500F9ABC0 /* RedirectHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedirectHandler.swift; sourceTree = ""; }; D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = ""; }; - DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HasImageComponent+Kingfisher.swift"; sourceTree = ""; }; F72CE9CD1FCF17ED00CC522A /* ImageModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageModifierTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -398,7 +396,6 @@ D12AB6AD215D2BB50013BA68 /* NSButton+Kingfisher.swift */, E9E3ED8A2B1F66B200734CFF /* HasImageComponent+Kingfisher.swift */, D12AB6AE215D2BB50013BA68 /* UIButton+Kingfisher.swift */, - DCEB2841257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift */, 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */, ); path = Extensions; @@ -874,7 +871,6 @@ 4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */, 3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */, D12AB710215D2BB50013BA68 /* KingfisherOptionsInfo.swift in Sources */, - DCEB2842257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 6f70af160..87489a428 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -30,39 +30,26 @@ public protocol KingfisherHasImageComponent: KingfisherCompatible { #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit - @available(macOS 13.0, *) extension NSComboButton: KingfisherHasImageComponent {} - @available(macOS 13.0, *) extension NSColorWell: 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 #if canImport(WatchKit) @@ -75,6 +62,11 @@ extension WKInterfaceImage: KingfisherHasImageComponent { } #endif +#if canImport(TVUIKit) +import TVUIKit +extension TVMonogramView: KingfisherHasImageComponent {} +#endif + @MainActor extension KingfisherWrapper where Base: KingfisherHasImageComponent { diff --git a/Sources/Extensions/TVMonogramView+Kingfisher.swift b/Sources/Extensions/TVMonogramView+Kingfisher.swift deleted file mode 100644 index da3738614..000000000 --- a/Sources/Extensions/TVMonogramView+Kingfisher.swift +++ /dev/null @@ -1,216 +0,0 @@ -// -// TVMonogramView+Kingfisher.swift -// Kingfisher -// -// Created by Marvin Nazari on 2020-12-07. -// -// Copyright (c) 2020 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. - -import Foundation - -#if canImport(TVUIKit) - -import TVUIKit - -@MainActor -@available(tvOS 12.0, *) -extension KingfisherWrapper where Base: TVMonogramView { - - // MARK: Setting Image - - /// Sets an image to the image view with a source. - /// - /// - Parameters: - /// - source: The `Source` object contains information about 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: (@MainActor @Sendable (Result) -> Void)? = nil - ) -> DownloadTask? - { - let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) - return setImage( - with: source, - placeholder: placeholder, - parsedOptions: options, - progressBlock: progressBlock, - completionHandler: completionHandler - ) - } - - func setImage( - with source: Source?, - placeholder: KFCrossPlatformImage? = nil, - parsedOptions: KingfisherParsedOptionsInfo, - progressBlock: DownloadProgressBlock? = nil, - completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil - ) -> DownloadTask? - { - var mutatingSelf = self - guard let source = 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: { task in - Task { @MainActor in mutatingSelf.imageTask = task } - }, - progressiveImageSetter: { self.base.image = $0 }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - 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 .success(let 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 - } - - /// Sets an image to the image view with a requested resource. - /// - /// - Parameters: - /// - resource: The `Resource` object contains information about 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 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: (@MainActor @Sendable (Result) -> Void)? = nil - ) -> DownloadTask? - { - return setImage( - with: resource?.convertToSource(), - placeholder: placeholder, - options: options, - progressBlock: progressBlock, - completionHandler: completionHandler) - } - - // MARK: Cancelling Image - - /// Cancel the image download task bounded to the image view if it is running. - /// Nothing will happen if the downloading has already finished. - public func cancelDownloadTask() { - imageTask?.cancel() - } -} - -@MainActor private var taskIdentifierKey: Void? -@MainActor private var imageTaskKey: Void? - -// MARK: Properties -@MainActor -@available(tvOS 12.0, *) -extension KingfisherWrapper where Base: TVMonogramView { - - public private(set) var taskIdentifier: Source.Identifier.Value? { - get { - let box: Box? = 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)} - } -} - -#endif From 054767f827c5f2e3fbc59a390a798889b29f81a4 Mon Sep 17 00:00:00 2001 From: onevcat Date: Wed, 8 May 2024 21:09:30 +0900 Subject: [PATCH 17/24] Refactor to split setter caller and actual setting --- .../HasImageComponent+Kingfisher.swift | 69 ++++++++++++++++--- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 87489a428..6f8488674 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -24,10 +24,30 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -public protocol KingfisherHasImageComponent: KingfisherCompatible { +public protocol KingfisherImageSettable: KingfisherCompatible { + @MainActor func setImage( + _ image: KFCrossPlatformImage?, + options: KingfisherParsedOptionsInfo + ) + @MainActor func getImage() -> KFCrossPlatformImage? +} + +public protocol KingfisherHasImageComponent: KingfisherImageSettable { @MainActor var image: KFCrossPlatformImage? { set get } } +extension KingfisherHasImageComponent { + @MainActor + public func setImage(_ image: KFCrossPlatformImage?, options: KingfisherParsedOptionsInfo) { + self.image = image + } + + @MainActor + public func getImage() -> KFCrossPlatformImage? { + image + } +} + #if canImport(AppKit) && !targetEnvironment(macCatalyst) import AppKit @available(macOS 13.0, *) @@ -67,8 +87,13 @@ import TVUIKit extension TVMonogramView: KingfisherHasImageComponent {} #endif +struct ImagePropertyAccessor: Sendable { + let setImage: @Sendable @MainActor (ImageType?, KingfisherParsedOptionsInfo) -> Void + let getImage: @Sendable @MainActor () -> ImageType? +} + @MainActor -extension KingfisherWrapper where Base: KingfisherHasImageComponent { +extension KingfisherWrapper where Base: KingfisherImageSettable { // MARK: Setting Image @@ -119,7 +144,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { { let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) return setImage( - with: source, + with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, @@ -321,9 +346,33 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { completionHandler: completionHandler ) } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil + ) -> DownloadTask? { + return setImage( + with: source, + imageAccessor: .init( + setImage: { base.setImage($0, options: $1) }, + getImage: { base.getImage() } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } +} +@MainActor +extension KingfisherWrapper { func setImage( with source: Source?, + imageAccessor: ImagePropertyAccessor, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, @@ -332,7 +381,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { { var mutatingSelf = self guard let source = source else { - base.image = placeholder + imageAccessor.setImage(placeholder, parsedOptions) mutatingSelf.taskIdentifier = nil completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil @@ -344,10 +393,10 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { #if os(watchOS) let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading #else - let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading || base.image == nil + let usePlaceholderDuringLoading = !options.keepCurrentImageWhileLoading || imageAccessor.getImage() == nil #endif if usePlaceholderDuringLoading { - base.image = placeholder + imageAccessor.setImage(placeholder, options) } let issuedIdentifier = Source.Identifier.next() @@ -363,7 +412,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { downloadTaskUpdated: { task in Task { @MainActor in mutatingSelf.imageTask = task } }, - progressiveImageSetter: { self.base.image = $0 }, + progressiveImageSetter: { imageAccessor.setImage($0, options) }, referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, completionHandler: { result in CallbackQueueMain.currentOrAsync { @@ -385,10 +434,10 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { switch result { case .success(let value): - self.base.image = value.image + imageAccessor.setImage(value.image, options) case .failure: if let image = options.onFailureImage { - self.base.image = image + imageAccessor.setImage(image, options) } } completionHandler?(result) @@ -414,7 +463,7 @@ extension KingfisherWrapper where Base: KingfisherHasImageComponent { @MainActor private var imageTaskKey: Void? @MainActor -extension KingfisherWrapper where Base: KingfisherHasImageComponent { +extension KingfisherWrapper { // MARK: Properties public private(set) var taskIdentifier: Source.Identifier.Value? { From ebe47431bcbb5b8c5c9099578ed04c7e87838f2e Mon Sep 17 00:00:00 2001 From: onevcat Date: Wed, 15 May 2024 00:18:18 +0900 Subject: [PATCH 18/24] Extract image task and identifier to setter --- .../HasImageComponent+Kingfisher.swift | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 6f8488674..d2199398e 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -92,6 +92,12 @@ struct ImagePropertyAccessor: Sendable { let getImage: @Sendable @MainActor () -> ImageType? } +struct TaskPropertyAccessor: Sendable { + let setTaskIdentifier: @Sendable @MainActor (TaskIdentifierKey?, Source.Identifier.Value?) -> Void + let getTaskIdentifier: @Sendable @MainActor (TaskIdentifierKey?) -> Source.Identifier.Value? + let setTask: @Sendable @MainActor (DownloadTask?) -> Void +} + @MainActor extension KingfisherWrapper where Base: KingfisherImageSettable { @@ -360,6 +366,19 @@ extension KingfisherWrapper where Base: KingfisherImageSettable { setImage: { base.setImage($0, options: $1) }, getImage: { base.getImage() } ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { _, value in + var mutatingSelf = self + mutatingSelf.taskIdentifier = value + }, + getTaskIdentifier: { _ in + self.taskIdentifier + }, + setTask: { task in + var mutatingSelf = self + mutatingSelf.imageTask = task + } + ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, @@ -370,19 +389,19 @@ extension KingfisherWrapper where Base: KingfisherImageSettable { @MainActor extension KingfisherWrapper { - func setImage( + func setImage( with source: Source?, imageAccessor: ImagePropertyAccessor, + taskAccessor: TaskPropertyAccessor, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil ) -> DownloadTask? { - var mutatingSelf = self guard let source = source else { imageAccessor.setImage(placeholder, parsedOptions) - mutatingSelf.taskIdentifier = nil + taskAccessor.setTaskIdentifier(nil, nil) completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } @@ -400,7 +419,7 @@ extension KingfisherWrapper { } let issuedIdentifier = Source.Identifier.next() - mutatingSelf.taskIdentifier = issuedIdentifier + taskAccessor.setTaskIdentifier(nil, issuedIdentifier) if let block = progressBlock { options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] @@ -410,13 +429,13 @@ extension KingfisherWrapper { with: source, options: options, downloadTaskUpdated: { task in - Task { @MainActor in mutatingSelf.imageTask = task } + Task { @MainActor in taskAccessor.setTask(task) } }, progressiveImageSetter: { imageAccessor.setImage($0, options) }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + referenceTaskIdentifierChecker: { issuedIdentifier == taskAccessor.getTaskIdentifier(nil) }, completionHandler: { result in CallbackQueueMain.currentOrAsync { - guard issuedIdentifier == self.taskIdentifier else { + guard issuedIdentifier == taskAccessor.getTaskIdentifier(nil) else { let reason: KingfisherError.ImageSettingErrorReason do { let value = try result.get() @@ -429,8 +448,8 @@ extension KingfisherWrapper { return } - mutatingSelf.imageTask = nil - mutatingSelf.taskIdentifier = nil + taskAccessor.setTask(nil) + taskAccessor.setTaskIdentifier(nil, nil) switch result { case .success(let value): @@ -444,18 +463,9 @@ extension KingfisherWrapper { } } ) - mutatingSelf.imageTask = task + taskAccessor.setTask(task) return task } - - // MARK: Cancelling Downloading Task - - /// Cancels the image download task of the image view if it is running. - /// - /// Nothing will happen if the downloading has already finished. - public func cancelDownloadTask() { - imageTask?.cancel() - } } // MARK: - Associated Object @@ -463,7 +473,7 @@ extension KingfisherWrapper { @MainActor private var imageTaskKey: Void? @MainActor -extension KingfisherWrapper { +extension KingfisherWrapper where Base: KingfisherImageSettable { // MARK: Properties public private(set) var taskIdentifier: Source.Identifier.Value? { @@ -481,4 +491,11 @@ extension KingfisherWrapper { get { return getAssociatedObject(base, &imageTaskKey) } set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} } + + /// Cancels the image download task of the image view if it is running. + /// + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } } From cda2849ffc21c656059fc0e706a03e625a5d2663 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 2 Jun 2024 15:47:02 +0900 Subject: [PATCH 19/24] Simplify the NSButton set image with generic method --- .../HasImageComponent+Kingfisher.swift | 2 +- Sources/Extensions/NSButton+Kingfisher.swift | 168 +++++------------- 2 files changed, 41 insertions(+), 129 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index d2199398e..680ae0700 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -362,7 +362,7 @@ extension KingfisherWrapper where Base: KingfisherImageSettable { ) -> DownloadTask? { return setImage( with: source, - imageAccessor: .init( + imageAccessor: ImagePropertyAccessor( setImage: { base.setImage($0, options: $1) }, getImage: { base.getImage() } ), diff --git a/Sources/Extensions/NSButton+Kingfisher.swift b/Sources/Extensions/NSButton+Kingfisher.swift index 9970bd1ae..24955a2ee 100644 --- a/Sources/Extensions/NSButton+Kingfisher.swift +++ b/Sources/Extensions/NSButton+Kingfisher.swift @@ -108,68 +108,28 @@ extension KingfisherWrapper where Base: NSButton { ) -> DownloadTask? { var mutatingSelf = self - guard let source = 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( + return setImage( with: source, - options: options, - downloadTaskUpdated: { task in - Task { @MainActor in mutatingSelf.imageTask = task } - }, - progressiveImageSetter: { self.base.image = $0 }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - 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 .success(let value): - self.base.image = value.image - completionHandler?(result) - - case .failure: - if let image = options.onFailureImage { - self.base.image = image - } - completionHandler?(result) - } - } - } + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + base.image = image + }, getImage: { + base.image + }), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { _, id in + mutatingSelf.taskIdentifier = id + }, + getTaskIdentifier: { _ in + mutatingSelf.taskIdentifier + }, setTask: { task in + mutatingSelf.imageTask = task + }), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler ) - - mutatingSelf.imageTask = task - return task } // MARK: Cancelling Downloading Task @@ -242,76 +202,28 @@ extension KingfisherWrapper where Base: NSButton { ) -> DownloadTask? { var mutatingSelf = self - guard let source = source else { - base.alternateImage = placeholder - mutatingSelf.alternateTaskIdentifier = nil - completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) - return nil - } - - var options = parsedOptions - if !options.keepCurrentImageWhileLoading { - base.alternateImage = placeholder - } - - let issuedIdentifier = Source.Identifier.next() - mutatingSelf.alternateTaskIdentifier = issuedIdentifier - - if let block = progressBlock { - options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] - } - - if let provider = ImageProgressiveProvider(options: options, refresh: { image in - self.base.alternateImage = image - }) { - options.onDataReceived = (options.onDataReceived ?? []) + [provider] - } - - options.onDataReceived?.forEach { - $0.onShouldApply = { issuedIdentifier == self.alternateTaskIdentifier } - } - - let task = KingfisherManager.shared.retrieveImage( + return setImage( with: source, - options: options, - downloadTaskUpdated: { task in - Task { @MainActor in mutatingSelf.alternateImageTask = task } - }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - guard issuedIdentifier == self.alternateTaskIdentifier 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.alternateImageTask = nil - mutatingSelf.alternateTaskIdentifier = nil - - switch result { - case .success(let value): - self.base.alternateImage = value.image - completionHandler?(result) - - case .failure: - if let image = options.onFailureImage { - self.base.alternateImage = image - } - completionHandler?(result) - } - } - } + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + base.alternateImage = image + }, getImage: { + base.alternateImage + }), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { _, id in + mutatingSelf.alternateTaskIdentifier = id + }, + getTaskIdentifier: { _ in + mutatingSelf.alternateTaskIdentifier + }, setTask: { task in + mutatingSelf.alternateImageTask = task + }), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler ) - - mutatingSelf.alternateImageTask = task - return task } // MARK: Cancelling Alternate Image Downloading Task From d77a40a5fffa00949a491512fbdb7c25e41ff55b Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 2 Jun 2024 15:50:45 +0900 Subject: [PATCH 20/24] Update xcode versions --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c093458d0..6e1249d9d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,8 +14,8 @@ jobs: 'watchOS Simulator,name=Apple Watch Series 8 (41mm)' ] xcode: [ - '15.2', - '15.3' + '15.3', + '15.4' ] steps: - uses: actions/checkout@v4 From 94a28287e3af3e181dbb9e3b3c5c11b386415949 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 2 Jun 2024 15:52:31 +0900 Subject: [PATCH 21/24] Do not use ruby setup --- .github/workflows/build.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6e1249d9d..ab3a90711 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,10 +19,8 @@ jobs: ] steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - self-hosted: true + - name: Install Gems + run: bundle install - name: Run tests env: DESTINATION: platform=${{ matrix.destination }} From 7ecfe0ab3ba858b205f468946d7982930287a466 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 2 Jun 2024 15:53:42 +0900 Subject: [PATCH 22/24] Update ruby version --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index 15a279981..bea438e9a 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.0 +3.3.1 From 3479f01e568ae10742af1cb6e3bcfb6cdc1e700e Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 2 Jun 2024 22:07:47 +0900 Subject: [PATCH 23/24] Simplify identifier setting and adopt for button types --- .../HasImageComponent+Kingfisher.swift | 30 ++-- Sources/Extensions/NSButton+Kingfisher.swift | 27 ++-- Sources/Extensions/UIButton+Kingfisher.swift | 152 ++++-------------- 3 files changed, 54 insertions(+), 155 deletions(-) diff --git a/Sources/Extensions/HasImageComponent+Kingfisher.swift b/Sources/Extensions/HasImageComponent+Kingfisher.swift index 680ae0700..b36bef56e 100644 --- a/Sources/Extensions/HasImageComponent+Kingfisher.swift +++ b/Sources/Extensions/HasImageComponent+Kingfisher.swift @@ -92,9 +92,9 @@ struct ImagePropertyAccessor: Sendable { let getImage: @Sendable @MainActor () -> ImageType? } -struct TaskPropertyAccessor: Sendable { - let setTaskIdentifier: @Sendable @MainActor (TaskIdentifierKey?, Source.Identifier.Value?) -> Void - let getTaskIdentifier: @Sendable @MainActor (TaskIdentifierKey?) -> Source.Identifier.Value? +struct TaskPropertyAccessor: Sendable { + let setTaskIdentifier: @Sendable @MainActor (Source.Identifier.Value?) -> Void + let getTaskIdentifier: @Sendable @MainActor () -> Source.Identifier.Value? let setTask: @Sendable @MainActor (DownloadTask?) -> Void } @@ -366,14 +366,12 @@ extension KingfisherWrapper where Base: KingfisherImageSettable { setImage: { base.setImage($0, options: $1) }, getImage: { base.getImage() } ), - taskAccessor: TaskPropertyAccessor( - setTaskIdentifier: { _, value in + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { var mutatingSelf = self - mutatingSelf.taskIdentifier = value + mutatingSelf.taskIdentifier = $0 }, - getTaskIdentifier: { _ in - self.taskIdentifier - }, + getTaskIdentifier: { self.taskIdentifier }, setTask: { task in var mutatingSelf = self mutatingSelf.imageTask = task @@ -389,10 +387,10 @@ extension KingfisherWrapper where Base: KingfisherImageSettable { @MainActor extension KingfisherWrapper { - func setImage( + func setImage( with source: Source?, imageAccessor: ImagePropertyAccessor, - taskAccessor: TaskPropertyAccessor, + taskAccessor: TaskPropertyAccessor, placeholder: KFCrossPlatformImage? = nil, parsedOptions: KingfisherParsedOptionsInfo, progressBlock: DownloadProgressBlock? = nil, @@ -401,7 +399,7 @@ extension KingfisherWrapper { { guard let source = source else { imageAccessor.setImage(placeholder, parsedOptions) - taskAccessor.setTaskIdentifier(nil, nil) + taskAccessor.setTaskIdentifier(nil) completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) return nil } @@ -419,7 +417,7 @@ extension KingfisherWrapper { } let issuedIdentifier = Source.Identifier.next() - taskAccessor.setTaskIdentifier(nil, issuedIdentifier) + taskAccessor.setTaskIdentifier(issuedIdentifier) if let block = progressBlock { options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] @@ -432,10 +430,10 @@ extension KingfisherWrapper { Task { @MainActor in taskAccessor.setTask(task) } }, progressiveImageSetter: { imageAccessor.setImage($0, options) }, - referenceTaskIdentifierChecker: { issuedIdentifier == taskAccessor.getTaskIdentifier(nil) }, + referenceTaskIdentifierChecker: { issuedIdentifier == taskAccessor.getTaskIdentifier() }, completionHandler: { result in CallbackQueueMain.currentOrAsync { - guard issuedIdentifier == taskAccessor.getTaskIdentifier(nil) else { + guard issuedIdentifier == taskAccessor.getTaskIdentifier() else { let reason: KingfisherError.ImageSettingErrorReason do { let value = try result.get() @@ -449,7 +447,7 @@ extension KingfisherWrapper { } taskAccessor.setTask(nil) - taskAccessor.setTaskIdentifier(nil, nil) + taskAccessor.setTaskIdentifier(nil) switch result { case .success(let value): diff --git a/Sources/Extensions/NSButton+Kingfisher.swift b/Sources/Extensions/NSButton+Kingfisher.swift index 24955a2ee..eb42e804d 100644 --- a/Sources/Extensions/NSButton+Kingfisher.swift +++ b/Sources/Extensions/NSButton+Kingfisher.swift @@ -116,15 +116,10 @@ extension KingfisherWrapper where Base: NSButton { }, getImage: { base.image }), - taskAccessor: TaskPropertyAccessor( - setTaskIdentifier: { _, id in - mutatingSelf.taskIdentifier = id - }, - getTaskIdentifier: { _ in - mutatingSelf.taskIdentifier - }, setTask: { task in - mutatingSelf.imageTask = task - }), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 }, + getTaskIdentifier: { mutatingSelf.taskIdentifier }, + setTask: { mutatingSelf.imageTask = $0 }), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, @@ -210,15 +205,11 @@ extension KingfisherWrapper where Base: NSButton { }, getImage: { base.alternateImage }), - taskAccessor: TaskPropertyAccessor( - setTaskIdentifier: { _, id in - mutatingSelf.alternateTaskIdentifier = id - }, - getTaskIdentifier: { _ in - mutatingSelf.alternateTaskIdentifier - }, setTask: { task in - mutatingSelf.alternateImageTask = task - }), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { mutatingSelf.alternateTaskIdentifier = $0 }, + getTaskIdentifier: { mutatingSelf.alternateTaskIdentifier }, + setTask: { mutatingSelf.alternateImageTask = $0 } + ), placeholder: placeholder, parsedOptions: parsedOptions, progressBlock: progressBlock, diff --git a/Sources/Extensions/UIButton+Kingfisher.swift b/Sources/Extensions/UIButton+Kingfisher.swift index 703a9298c..d43d9a3a1 100644 --- a/Sources/Extensions/UIButton+Kingfisher.swift +++ b/Sources/Extensions/UIButton+Kingfisher.swift @@ -114,69 +114,23 @@ extension KingfisherWrapper where Base: UIButton { progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { - guard let source = source else { - base.setImage(placeholder, for: state) - setTaskIdentifier(nil, for: state) - completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) - return nil - } - - var options = parsedOptions - if !options.keepCurrentImageWhileLoading { - base.setImage(placeholder, for: state) - } - var mutatingSelf = self - let issuedIdentifier = Source.Identifier.next() - setTaskIdentifier(issuedIdentifier, for: state) - - if let block = progressBlock { - options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] - } - - let task = KingfisherManager.shared.retrieveImage( + return setImage( with: source, - options: options, - downloadTaskUpdated: { task in - Task { @MainActor in mutatingSelf.imageTask = task } - }, - progressiveImageSetter: { self.base.setImage($0, for: state) }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier(for: state) }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - guard issuedIdentifier == self.taskIdentifier(for: state) 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.setTaskIdentifier(nil, for: state) - - switch result { - case .success(let value): - self.base.setImage(value.image, for: state) - completionHandler?(result) - - case .failure: - if let image = options.onFailureImage { - self.base.setImage(image, for: state) - } - completionHandler?(result) - } - } - } + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in base.setImage(image, for: state) }, + getImage: { base.image(for: state) } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { setTaskIdentifier($0, for: state) }, + getTaskIdentifier: { taskIdentifier(for: state) }, + setTask: { mutatingSelf.imageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler ) - - mutatingSelf.imageTask = task - return task } // MARK: Cancelling Downloading Task @@ -269,71 +223,27 @@ extension KingfisherWrapper where Base: UIButton { progressBlock: DownloadProgressBlock? = nil, completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { - guard let source = source else { - base.setBackgroundImage(placeholder, for: state) - setBackgroundTaskIdentifier(nil, for: state) - completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) - return nil - } - - var options = parsedOptions - if !options.keepCurrentImageWhileLoading { - base.setBackgroundImage(placeholder, for: state) - } - var mutatingSelf = self - let issuedIdentifier = Source.Identifier.next() - setBackgroundTaskIdentifier(issuedIdentifier, for: state) - - if let block = progressBlock { - options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] - } - - let task = KingfisherManager.shared.retrieveImage( + return setImage( with: source, - options: options, - downloadTaskUpdated: { task in - Task { @MainActor in - mutatingSelf.backgroundImageTask = task - } - }, - progressiveImageSetter: { self.base.setBackgroundImage($0, for: state) }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.backgroundTaskIdentifier(for: state) }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - guard issuedIdentifier == self.backgroundTaskIdentifier(for: state) 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.backgroundImageTask = nil - mutatingSelf.setBackgroundTaskIdentifier(nil, for: state) - - switch result { - case .success(let value): - self.base.setBackgroundImage(value.image, for: state) - completionHandler?(result) - - case .failure: - if let image = options.onFailureImage { - self.base.setBackgroundImage(image, for: state) - } - completionHandler?(result) - } + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + base.setBackgroundImage(image, for: state) + }, + getImage: { + base.backgroundImage(for: state) } - } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { setBackgroundTaskIdentifier($0, for: state) }, + getTaskIdentifier: { backgroundTaskIdentifier(for: state) }, + setTask: { mutatingSelf.backgroundImageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler ) - - mutatingSelf.backgroundImageTask = task - return task } // MARK: Cancelling Background Downloading Task From 9666885272dcd203d7f74c3c6f5ddf4967312d97 Mon Sep 17 00:00:00 2001 From: onevcat Date: Sun, 2 Jun 2024 22:52:11 +0900 Subject: [PATCH 24/24] Use new set image for CPListItem --- .../Extensions/CPListItem+Kingfisher.swift | 140 ++++-------------- 1 file changed, 28 insertions(+), 112 deletions(-) diff --git a/Sources/Extensions/CPListItem+Kingfisher.swift b/Sources/Extensions/CPListItem+Kingfisher.swift index 462144f25..a2bf477df 100644 --- a/Sources/Extensions/CPListItem+Kingfisher.swift +++ b/Sources/Extensions/CPListItem+Kingfisher.swift @@ -110,123 +110,39 @@ extension KingfisherWrapper where Base: CPListItem { completionHandler: (@MainActor @Sendable (Result) -> Void)? = nil) -> DownloadTask? { var mutatingSelf = self - guard let source = source else { - /** - * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 - * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, - * which allows >=14.5 SDK to set a `nil` image. This compile check allows - * newer SDK users to set the image to `nil`, while still allowing older SDK - * users to compile the framework. - */ - #if compiler(>=5.4) - self.base.setImage(placeholder) - #else - if let placeholder = placeholder { - self.base.setImage(placeholder) - } - #endif - - mutatingSelf.taskIdentifier = nil - completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) - return nil - } - - var options = parsedOptions - if !options.keepCurrentImageWhileLoading { - /** - * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 - * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, - * which allows >=14.5 SDK to set a `nil` image. This compile check allows - * newer SDK users to set the image to `nil`, while still allowing older SDK - * users to compile the framework. - */ - #if compiler(>=5.4) - self.base.setImage(placeholder) - #else // Let older SDK users deal with the older behavior. - if let placeholder = placeholder { - self.base.setImage(placeholder) - } - #endif - } - - let issuedIdentifier = Source.Identifier.next() - mutatingSelf.taskIdentifier = issuedIdentifier - - if let block = progressBlock { - options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] - } - - let task = KingfisherManager.shared.retrieveImage( + return setImage( with: source, - options: options, - downloadTaskUpdated: { task in - Task { @MainActor in mutatingSelf.imageTask = task } - }, - progressiveImageSetter: { image in - /** - * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 - * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, - * which allows >=14.5 SDK to set a `nil` image. This compile check allows - * newer SDK users to set the image to `nil`, while still allowing older SDK - * users to compile the framework. - */ - #if compiler(>=5.4) - self.base.setImage(image) - #else // Let older SDK users deal with the older behavior. - if let image = image { + imageAccessor: ImagePropertyAccessor( + setImage: { image, _ in + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) self.base.setImage(image) - } - #endif - }, - referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, - completionHandler: { result in - CallbackQueueMain.currentOrAsync { - 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 .success(let value): - self.base.setImage(value.image) - completionHandler?(result) - - case .failure: - if let image = options.onFailureImage { - /** - * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 - * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, - * which allows >=14.5 SDK to set a `nil` image. This compile check allows - * newer SDK users to set the image to `nil`, while still allowing older SDK - * users to compile the framework. - */ - #if compiler(>=5.4) - self.base.setImage(image) - #else // Let older SDK users deal with the older behavior. - if let unwrapped = image { - self.base.setImage(unwrapped) - } - #endif - } - completionHandler?(result) + #else + if let image = image { + self.base.setImage(image) } + #endif + }, + getImage: { + self.base.image } - } + ), + taskAccessor: TaskPropertyAccessor( + setTaskIdentifier: { mutatingSelf.taskIdentifier = $0 }, + getTaskIdentifier: { mutatingSelf.taskIdentifier }, + setTask: { mutatingSelf.imageTask = $0 } + ), + placeholder: placeholder, + parsedOptions: parsedOptions, + progressBlock: progressBlock, + completionHandler: completionHandler ) - - mutatingSelf.imageTask = task - return task } // MARK: Cancelling Image