소스 검색

Merge branch 'master' into master

Wei Wang 7 달 전
부모
커밋
8e41dd9a85

+ 80 - 0
Demo/Demo/Kingfisher-Demo/SwiftUIViews/LoadingFailureDemo.swift

@@ -0,0 +1,80 @@
+//
+//  LoadingFailureDemo.swift
+//  Kingfisher
+//
+//  Created by onevcat on 2025/06/29.
+//
+//  Copyright (c) 2025 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.
+
+import SwiftUI
+import Kingfisher
+
+@available(iOS 14.0, *)
+struct LoadingFailureDemo: View {
+
+    var url: URL {
+        URL(string: "https://example.com")!
+    }
+    
+    var warningImage: UIImage {
+        let config = UIImage.SymbolConfiguration(pointSize: 50)
+        return UIImage(
+            systemName: "wrongwaysign",
+            withConfiguration: config
+        )!
+    }
+    
+    var body: some View {
+        VStack {
+            KFImage(url)
+                .onFailureImage(warningImage) // onFailureImage should not work
+                .onFailureView {
+                    ZStack {
+                        RoundedRectangle(cornerRadius: 20)
+                            .fill(Color.red.opacity(0.5))
+                        Image(systemName: "exclamationmark.triangle.fill")
+                            .resizable()
+                            .frame(width: 50, height: 47)
+                            .foregroundColor(.yellow)
+                    }
+                }
+                .frame(width: 200, height: 200)
+            Text("onFailureView")
+            Spacer().frame(height: 20)
+            
+            KFImage(url)
+                .onFailureImage(warningImage)
+                .frame(width: 200, height: 200)
+                .background(
+                    RoundedRectangle(cornerRadius: 20)
+                        .fill(Color.red.opacity(0.5))
+                )
+            Text("onFailureImage")
+        }
+    }
+}
+
+@available(iOS 14.0, *)
+struct LoadingFailureDemo_Previews: PreviewProvider {
+    static var previews: some View {
+        LoadingFailureDemo()
+    }
+}

+ 1 - 0
Demo/Demo/Kingfisher-Demo/SwiftUIViews/MainView.swift

@@ -53,6 +53,7 @@ struct MainView: View {
                 NavigationLink(destination: GeometryReaderDemo()) { Text("Geometry Reader") }
                 NavigationLink(destination: TransitionViewDemo()) { Text("Transition") }
                 NavigationLink(destination: ProgressiveJPEGDemo()) { Text("Progressive JPEG") }
+                NavigationLink(destination: LoadingFailureDemo()) { Text("Loading Failure") }
             }
             
             Section(header: Text("Regression Cases")) {

+ 4 - 0
Demo/Kingfisher-Demo.xcodeproj/project.pbxproj

@@ -55,6 +55,7 @@
 		D12EB83E24DD902300329EE1 /* TextAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12EB83D24DD902300329EE1 /* TextAttachmentViewController.swift */; };
 		D12EB84024DDB9E100329EE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */; };
 		D12F67682CB10AE000AB63AB /* LivePhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */; };
+		D14799D92E1129A900053537 /* LoadingFailureDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14799D82E1129A800053537 /* LoadingFailureDemo.swift */; };
 		D1679A461C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
 		D16AAF282D5247CF00E7F764 /* Issue2352View.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16AAF272D5247CA00E7F764 /* Issue2352View.swift */; };
 		D16CC3D824E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16CC3D724E03FEA00F1A515 /* AVAssetImageGeneratorViewController.swift */; };
@@ -209,6 +210,7 @@
 		D12EB83F24DDB9E000329EE1 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
 		D12F67672CB10AD900AB63AB /* LivePhotoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LivePhotoViewController.swift; sourceTree = "<group>"; };
 		D13F49C21BEDA53F00CE335D /* Kingfisher-tvOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-tvOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		D14799D82E1129A800053537 /* LoadingFailureDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingFailureDemo.swift; sourceTree = "<group>"; };
 		D16218A4238EAA67004A1C6C /* Kingfisher-Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Kingfisher-Demo.entitlements"; sourceTree = "<group>"; };
 		D1679A391C4E78B20020FD12 /* Kingfisher-watchOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-watchOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
 		D1679A451C4E78B20020FD12 /* Kingfisher-watchOS-Demo Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Kingfisher-watchOS-Demo Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -453,6 +455,7 @@
 			children = (
 				4BC0ED4829A6EE4F003E9CD1 /* Regression */,
 				D1F78A622589F17200930759 /* MainView.swift */,
+				D14799D82E1129A800053537 /* LoadingFailureDemo.swift */,
 				D1F78A612589F17200930759 /* ListDemo.swift */,
 				D198F42125EDC4B900C53E0D /* GridDemo.swift */,
 				4B779C8426743C2800FF9C1E /* GeometryReaderDemo.swift */,
@@ -724,6 +727,7 @@
 				072922432638639D0089E810 /* AnimatedImageDemo.swift in Sources */,
 				4B6E1B6D28DB4E8C0023B54B /* Issue1998View.swift in Sources */,
 				D1A1CCA721A18A3200263AD8 /* UIViewController+KingfisherOperation.swift in Sources */,
+				D14799D92E1129A900053537 /* LoadingFailureDemo.swift in Sources */,
 				D1E612E22D75F9AC00DACD51 /* ProgressiveJPEGDemo.swift in Sources */,
 				4B92FE5625FF906B00473088 /* AutoSizingTableViewController.swift in Sources */,
 				D1F78A642589F17200930759 /* ListDemo.swift in Sources */,

+ 5 - 4
Sources/SwiftUI/ImageBinder.swift

@@ -70,11 +70,10 @@ extension KFImage {
             guard let source = context.source else {
                 CallbackQueueMain.currentOrAsync {
                     context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
-                    if let image = context.options.onFailureImage {
-                        self.loadedImage = image
-                    }
                     if let view = context.failureView {
                         self.failureView = view
+                    } else if let image = context.options.onFailureImage {
+                        self.loadedImage = image
                     }
                     self.loading = false
                     self.markLoaded(sendChangeEvent: false)
@@ -130,7 +129,9 @@ extension KFImage {
                             }
                         case .failure(let error):
                             CallbackQueueMain.currentOrAsync {
-                                if let image = context.options.onFailureImage {
+                                if let view = context.failureView {
+                                    self.failureView = view
+                                } else if let image = context.options.onFailureImage {
                                     self.loadedImage = image
                                 }
                                 if let view = context.failureView {

+ 23 - 2
Sources/SwiftUI/KFImageOptions.swift

@@ -125,8 +125,29 @@ extension KFImageProtocol {
 
     /// Sets a failure `View` that is displayed when the image fails to load.
     ///
-    /// - Parameter content: A view that represents failure.
-    /// - Returns: A Kingfisher-compatible image view that includes the provided `content` as its failure.
+    /// Use this modifier to provide a custom view when image loading fails. This offers more flexibility than
+    /// `onFailureImage` by allowing any SwiftUI view as the failure placeholder.
+    ///
+    /// Example:
+    /// ```swift
+    /// KFImage(url)
+    ///     .onFailureView {
+    ///         VStack {
+    ///             Image(systemName: "exclamationmark.triangle")
+    ///                 .foregroundColor(.red)
+    ///             Text("Failed to load image")
+    ///                 .font(.caption)
+    ///             Button("Retry") {
+    ///                 // Retry logic
+    ///             }
+    ///         }
+    ///     }
+    /// ```
+    ///
+    /// - Note: If both `onFailureImage` and `onFailureView` are set, `onFailureView` takes precedence.
+    /// 
+    /// - Parameter content: A view builder that creates the failure view.
+    /// - Returns: A Kingfisher-compatible image view that displays the provided `content` when image loading fails.
     public func onFailureView<F: View>(@ViewBuilder _ content: @escaping () -> F) -> Self {
         context.failureView = { AnyView(content()) }
         return self

+ 4 - 0
Sources/SwiftUI/KFImageRenderer.swift

@@ -46,8 +46,12 @@ struct KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView
             renderedImage().opacity(binder.loaded ? 1.0 : 0.0)
             if binder.loadedImage == nil {
                 ZStack {
+                    // Priority: failureView > placeholder > Color.clear
+                    // failureView is only set when image loading fails
                     if let failureView = binder.failureView {
                         failureView()
+                    } else if let placeholder = context.placeholder {
+                        placeholder(binder.progress)
                     } else {
                         if let placeholder = context.placeholder {
                             placeholder(binder.progress)