This commit is contained in:
DDIsFriend
2023-08-24 16:46:39 +08:00
parent 887a468768
commit 1a0943017a
139 changed files with 24162 additions and 13640 deletions

View File

@@ -0,0 +1,149 @@
//
// ImageBinder.swift
// Kingfisher
//
// Created by onevcat on 2019/06/27.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine)
import SwiftUI
import Combine
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImage {
/// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs
/// image downloading and progress reporting based on `KingfisherManager`.
class ImageBinder: ObservableObject {
init() {}
var downloadTask: DownloadTask?
private var loading = false
var loadingOrSucceeded: Bool {
return loading || loadedImage != nil
}
// Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once
// we can drop iOS 12.
private(set) var loaded = false
private(set) var animating = false
var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } }
var progress: Progress = .init()
func markLoading() {
loading = true
}
func markLoaded(sendChangeEvent: Bool) {
loaded = true
if sendChangeEvent {
objectWillChange.send()
}
}
func start<HoldingView: KFImageHoldingView>(context: Context<HoldingView>) {
guard let source = context.source else {
CallbackQueue.mainCurrentOrAsync.execute {
context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
if let image = context.options.onFailureImage {
self.loadedImage = image
}
self.loading = false
self.markLoaded(sendChangeEvent: false)
}
return
}
loading = true
progress = .init()
downloadTask = KingfisherManager.shared
.retrieveImage(
with: source,
options: context.options,
progressBlock: { size, total in
self.updateProgress(downloaded: size, total: total)
context.onProgressDelegate.call((size, total))
},
completionHandler: { [weak self] result in
guard let self = self else { return }
CallbackQueue.mainCurrentOrAsync.execute {
self.downloadTask = nil
self.loading = false
}
switch result {
case .success(let value):
CallbackQueue.mainCurrentOrAsync.execute {
if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) {
self.animating = true
let animation = Animation.linear(duration: fadeDuration)
withAnimation(animation) {
// Trigger the view render to apply the animation.
self.markLoaded(sendChangeEvent: true)
}
} else {
self.markLoaded(sendChangeEvent: false)
}
self.loadedImage = value.image
self.animating = false
}
CallbackQueue.mainAsync.execute {
context.onSuccessDelegate.call(value)
}
case .failure(let error):
CallbackQueue.mainCurrentOrAsync.execute {
if let image = context.options.onFailureImage {
self.loadedImage = image
}
self.markLoaded(sendChangeEvent: true)
}
CallbackQueue.mainAsync.execute {
context.onFailureDelegate.call(error)
}
}
})
}
private func updateProgress(downloaded: Int64, total: Int64) {
progress.totalUnitCount = total
progress.completedUnitCount = downloaded
objectWillChange.send()
}
/// Cancels the download task if it is in progress.
func cancel() {
downloadTask?.cancel()
downloadTask = nil
loading = false
}
}
}
#endif

View File

@@ -0,0 +1,102 @@
//
// ImageContext.swift
// Kingfisher
//
// Created by onevcat on 2021/05/08.
//
// Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine)
import SwiftUI
import Combine
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImage {
public class Context<HoldingView: KFImageHoldingView> {
let source: Source?
var options = KingfisherParsedOptionsInfo(
KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously]
)
var configurations: [(HoldingView) -> HoldingView] = []
var renderConfigurations: [(HoldingView.RenderingView) -> Void] = []
var contentConfiguration: ((HoldingView) -> AnyView)? = nil
var cancelOnDisappear: Bool = false
var placeholder: ((Progress) -> AnyView)? = nil
let onFailureDelegate = Delegate<KingfisherError, Void>()
let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()
let onProgressDelegate = Delegate<(Int64, Int64), Void>()
var startLoadingBeforeViewAppear: Bool = false
init(source: Source?) {
self.source = source
}
func shouldApplyFade(cacheType: CacheType) -> Bool {
options.forceTransition || cacheType == .none
}
func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? {
shouldApplyFade(cacheType: cacheType)
? options.transition.fadeDuration
: nil
}
}
}
extension ImageTransition {
// Only for fade effect in SwiftUI.
fileprivate var fadeDuration: TimeInterval? {
switch self {
case .fade(let duration):
return duration
default:
return nil
}
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImage.Context: Hashable {
public static func == (lhs: KFImage.Context<HoldingView>, rhs: KFImage.Context<HoldingView>) -> Bool {
lhs.source == rhs.source &&
lhs.options.processor.identifier == rhs.options.processor.identifier
}
public func hash(into hasher: inout Hasher) {
hasher.combine(source)
hasher.combine(options.processor.identifier)
}
}
#if canImport(UIKit) && !os(watchOS)
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFAnimatedImage {
public typealias Context = KFImage.Context
typealias ImageBinder = KFImage.ImageBinder
}
#endif
#endif

View File

@@ -0,0 +1,96 @@
//
// KFAnimatedImage.swift
// Kingfisher
//
// Created by wangxingbin on 2021/4/29.
//
// Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine) && canImport(UIKit) && !os(watchOS)
import SwiftUI
import Combine
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public struct KFAnimatedImage: KFImageProtocol {
public typealias HoldingView = KFAnimatedImageViewRepresenter
public var context: Context<HoldingView>
public init(context: KFImage.Context<HoldingView>) {
self.context = context
}
/// Configures current rendering view with a `block`. This block will be applied when the under-hood
/// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)`
///
/// - Parameter block: The block applies to the animated image view.
/// - Returns: A `KFAnimatedImage` view that being configured by the `block`.
public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self {
context.renderConfigurations.append(block)
return self
}
}
/// A wrapped `UIViewRepresentable` of `AnimatedImageView`
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public struct KFAnimatedImageViewRepresenter: UIViewRepresentable, KFImageHoldingView {
public typealias RenderingView = AnimatedImageView
public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> KFAnimatedImageViewRepresenter {
KFAnimatedImageViewRepresenter(image: image, context: context)
}
var image: KFCrossPlatformImage?
let context: KFImage.Context<KFAnimatedImageViewRepresenter>
public func makeUIView(context: Context) -> AnimatedImageView {
let view = AnimatedImageView()
self.context.renderConfigurations.forEach { $0(view) }
view.image = image
// Allow SwiftUI scale (fit/fill) working fine.
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
return view
}
public func updateUIView(_ uiView: AnimatedImageView, context: Context) {
uiView.image = image
}
}
#if DEBUG
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
struct KFAnimatedImage_Previews: PreviewProvider {
static var previews: some View {
Group {
KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!))
.onSuccess { r in
print(r)
}
.placeholder {
ProgressView()
}
.padding()
}
}
}
#endif
#endif

View File

@@ -0,0 +1,106 @@
//
// KFImage.swift
// Kingfisher
//
// Created by onevcat on 2019/06/26.
//
// Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine)
import SwiftUI
import Combine
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public struct KFImage: KFImageProtocol {
public var context: Context<Image>
public init(context: Context<Image>) {
self.context = context
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Image: KFImageHoldingView {
public typealias RenderingView = Image
public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> Image {
Image(crossPlatformImage: image)
}
}
// MARK: - Image compatibility.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImage {
public func resizable(
capInsets: EdgeInsets = EdgeInsets(),
resizingMode: Image.ResizingMode = .stretch) -> KFImage
{
configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) }
}
public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage {
configure { $0.renderingMode(renderingMode) }
}
public func interpolation(_ interpolation: Image.Interpolation) -> KFImage {
configure { $0.interpolation(interpolation) }
}
public func antialiased(_ isAntialiased: Bool) -> KFImage {
configure { $0.antialiased(isAntialiased) }
}
/// Starts the loading process of `self` immediately.
///
/// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading
/// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a
/// flickering since the loading does not happen immediately. Call this method if you want to start the load at once
/// could help avoiding the flickering, with some performance trade-off.
///
/// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data.
/// It does nothing now and please just remove it.
///
/// - Returns: The `Self` value with changes applied.
@available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.")
public func loadImmediately(_ start: Bool = true) -> KFImage {
return self
}
}
#if DEBUG
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
struct KFImage_Previews: PreviewProvider {
static var previews: some View {
Group {
KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!)
.onSuccess { r in
print(r)
}
.placeholder { p in
ProgressView(p)
}
.resizable()
.aspectRatio(contentMode: .fit)
.padding()
}
}
}
#endif
#endif

View File

@@ -0,0 +1,158 @@
//
// KFImageOptions.swift
// Kingfisher
//
// Created by onevcat on 2020/12/20.
//
// Copyright (c) 2020 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine)
import SwiftUI
import Combine
// MARK: - KFImage creating.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImageProtocol {
/// Creates a `KFImage` for a given `Source`.
/// - Parameters:
/// - source: The `Source` object defines data information from network or a data provider.
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
public static func source(
_ source: Source?
) -> Self
{
Self.init(source: source)
}
/// Creates a `KFImage` for a given `Resource`.
/// - Parameters:
/// - source: The `Resource` object defines data information like key or URL.
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
public static func resource(
_ resource: Resource?
) -> Self
{
source(resource?.convertToSource())
}
/// Creates a `KFImage` for a given `URL`.
/// - Parameters:
/// - url: The URL where the image should be downloaded.
/// - cacheKey: The key used to store the downloaded image in cache.
/// If `nil`, the `absoluteString` of `url` is used as the cache key.
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
public static func url(
_ url: URL?, cacheKey: String? = nil
) -> Self
{
source(url?.convertToSource(overrideCacheKey: cacheKey))
}
/// Creates a `KFImage` for a given `ImageDataProvider`.
/// - Parameters:
/// - provider: The `ImageDataProvider` object contains information about the data.
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
public static func dataProvider(
_ provider: ImageDataProvider?
) -> Self
{
source(provider?.convertToSource())
}
/// Creates a builder for some given raw data and a cache key.
/// - Parameters:
/// - data: The data object from which the image should be created.
/// - cacheKey: The key used to store the downloaded image in cache.
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
public static func data(
_ data: Data?, cacheKey: String
) -> Self
{
if let data = data {
return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))
} else {
return dataProvider(nil)
}
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImageProtocol {
/// Sets a placeholder `View` which shows when loading the image, with a progress parameter as input.
/// - Parameter content: A view that describes the placeholder.
/// - Returns: A `KFImage` view that contains `content` as its placeholder.
public func placeholder<P: View>(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self {
context.placeholder = { progress in
return AnyView(content(progress))
}
return self
}
/// Sets a placeholder `View` which shows when loading the image.
/// - Parameter content: A view that describes the placeholder.
/// - Returns: A `KFImage` view that contains `content` as its placeholder.
public func placeholder<P: View>(@ViewBuilder _ content: @escaping () -> P) -> Self {
placeholder { _ in content() }
}
/// Sets cancelling the download task bound to `self` when the view disappearing.
/// - Parameter flag: Whether cancel the task or not.
/// - Returns: A `KFImage` view that cancels downloading task when disappears.
public func cancelOnDisappear(_ flag: Bool) -> Self {
context.cancelOnDisappear = flag
return self
}
/// Sets a fade transition for the image task.
/// - Parameter duration: The duration of the fade transition.
/// - Returns: A `KFImage` with changes applied.
///
/// Kingfisher will use the fade transition to animate the image in if it is downloaded from web.
/// The transition will not happen when the
/// image is retrieved from either memory or disk cache by default. If you need to do the transition even when
/// the image being retrieved from cache, also call `forceRefresh()` on the returned `KFImage`.
public func fade(duration: TimeInterval) -> Self {
context.options.transition = .fade(duration)
return self
}
/// Sets whether to start the image loading before the view actually appears.
///
/// By default, Kingfisher performs a lazy loading for `KFImage`. The image loading won't start until the view's
/// `onAppear` is called. However, sometimes you may want to trigger an aggressive loading for the view. By enabling
/// this, the `KFImage` will try to load the view when its `body` is evaluated when the image loading is not yet
/// started or a previous loading did fail.
///
/// - Parameter flag: Whether the image loading should happen before view appear. Default is `true`.
/// - Returns: A `KFImage` with changes applied.
///
/// - Note: This is a temporary workaround for an issue from iOS 16, where the SwiftUI view's `onAppear` is not
/// called when it is deeply embedded inside a `List` or `ForEach`.
/// See [#1988](https://github.com/onevcat/Kingfisher/issues/1988). It may cause performance regression, especially
/// if you have a lot of images to load in the view. Use it as your own risk.
///
public func startLoadingBeforeViewAppear(_ flag: Bool = true) -> Self {
context.startLoadingBeforeViewAppear = flag
return self
}
}
#endif

View File

@@ -0,0 +1,112 @@
//
// KFImageProtocol.swift
// Kingfisher
//
// Created by onevcat on 2021/05/08.
//
// Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine)
import SwiftUI
import Combine
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public protocol KFImageProtocol: View, KFOptionSetter {
associatedtype HoldingView: KFImageHoldingView
var context: KFImage.Context<HoldingView> { get set }
init(context: KFImage.Context<HoldingView>)
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImageProtocol {
public var body: some View {
ZStack {
KFImageRenderer<HoldingView>(
context: context
).id(context)
}
}
/// Creates a Kingfisher compatible image view to load image from the given `Source`.
/// - Parameters:
/// - source: The image `Source` defining where to load the target image.
public init(source: Source?) {
let context = KFImage.Context<HoldingView>(source: source)
self.init(context: context)
}
/// Creates a Kingfisher compatible image view to load image from the given `URL`.
/// - Parameters:
/// - source: The image `Source` defining where to load the target image.
public init(_ url: URL?) {
self.init(source: url?.convertToSource())
}
/// Configures current image with a `block` and return another `Image` to use as the final content.
///
/// This block will be lazily applied when creating the final `Image`.
///
/// If multiple `configure` modifiers are added to the image, they will be evaluated by order. If you want to
/// configure the input image (which is usually an `Image` value) to a non-`Image` value, use `contentConfigure`.
///
/// - Parameter block: The block applies to loaded image. The block should return an `Image` that is configured.
/// - Returns: A `KFImage` view that configures internal `Image` with `block`.
public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self {
context.configurations.append(block)
return self
}
/// Configures current image with a `block` and return a `View` to use as the final content.
///
/// This block will be lazily applied when creating the final `Image`.
///
/// If multiple `contentConfigure` modifiers are added to the image, only the last one will be stored and used.
///
/// - Parameter block: The block applies to the loaded image. The block should return a `View` that is configured.
/// - Returns: A `KFImage` view that configures internal `Image` with `block`.
public func contentConfigure<V: View>(_ block: @escaping (HoldingView) -> V) -> Self {
context.contentConfiguration = { AnyView(block($0)) }
return self
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public protocol KFImageHoldingView: View {
associatedtype RenderingView
static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> Self
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension KFImageProtocol {
public var options: KingfisherParsedOptionsInfo {
get { context.options }
nonmutating set { context.options = newValue }
}
public var onFailureDelegate: Delegate<KingfisherError, Void> { context.onFailureDelegate }
public var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { context.onSuccessDelegate }
public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate }
public var delegateObserver: AnyObject { context }
}
#endif

View File

@@ -0,0 +1,129 @@
//
// KFImageRenderer.swift
// Kingfisher
//
// Created by onevcat on 2021/05/08.
//
// Copyright (c) 2021 Wei Wang <onevcat@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#if canImport(SwiftUI) && canImport(Combine)
import SwiftUI
import Combine
/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`.
/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
struct KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView {
@StateObject var binder: KFImage.ImageBinder = .init()
let context: KFImage.Context<HoldingView>
var body: some View {
if context.startLoadingBeforeViewAppear && !binder.loadingOrSucceeded && !binder.animating {
binder.markLoading()
DispatchQueue.main.async { binder.start(context: context) }
}
return ZStack {
renderedImage().opacity(binder.loaded ? 1.0 : 0.0)
if binder.loadedImage == nil {
ZStack {
if let placeholder = context.placeholder {
placeholder(binder.progress)
} else {
Color.clear
}
}
.onAppear { [weak binder = self.binder] in
guard let binder = binder else {
return
}
if !binder.loadingOrSucceeded {
binder.start(context: context)
}
}
.onDisappear { [weak binder = self.binder] in
guard let binder = binder else {
return
}
if context.cancelOnDisappear {
binder.cancel()
}
}
}
}
// Workaround for https://github.com/onevcat/Kingfisher/issues/1988
// on iOS 16 there seems to be a bug that when in a List, the `onAppear` of the `ZStack` above in the
// `binder.loadedImage == nil` not get called. Adding this empty `onAppear` fixes it and the life cycle can
// work again.
//
// There is another "fix": adding an `else` clause and put a `Color.clear` there. But I believe this `onAppear`
// should work better.
//
// It should be a bug in iOS 16, I guess it is some kinds of over-optimization in list cell loading caused it.
.onAppear()
}
@ViewBuilder
private func renderedImage() -> some View {
let configuredImage = context.configurations
.reduce(HoldingView.created(from: binder.loadedImage, context: context)) {
current, config in config(current)
}
if let contentConfiguration = context.contentConfiguration {
contentConfiguration(configuredImage)
} else {
configuredImage
}
}
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension Image {
// Creates an Image with either UIImage or NSImage.
init(crossPlatformImage: KFCrossPlatformImage?) {
#if canImport(UIKit)
self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage())
#elseif canImport(AppKit)
self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage())
#endif
}
}
#if canImport(UIKit)
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension UIImage.Orientation {
func toSwiftUI() -> Image.Orientation {
switch self {
case .down: return .down
case .up: return .up
case .left: return .left
case .right: return .right
case .upMirrored: return .upMirrored
case .downMirrored: return .downMirrored
case .leftMirrored: return .leftMirrored
case .rightMirrored: return .rightMirrored
@unknown default: return .up
}
}
}
#endif
#endif