You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI加载动画仅在首张图片获取后停止,如何保持至全资源加载完成?

解决方案

问题出在每次单张图片加载完成后就直接将isLoading设为false,导致第一张图片加载完就关闭了加载动画。我们需要跟踪所有需要加载的资源状态,等全部完成后再隐藏加载动画。

具体修改步骤

  1. 添加加载状态跟踪变量:在WebViewStore中新增两个变量,记录需要加载的图片总数和已完成加载的数量,同时重置旧的图片数据避免缓存干扰。
  2. 修改加载初始化逻辑:调用loadURL时,初始化计数器并清空之前的图片数据。
  3. 更新图片加载完成后的逻辑:不再直接关闭加载动画,而是递增完成计数,当完成数等于总数时才关闭加载动画。
  4. 处理重试失败的边界情况:当重试次数耗尽时,强制关闭加载动画,避免无限加载。

修改后的完整代码

import SwiftUI
import WebKit

struct ContentView: View {
    @StateObject var webViewStore = WebViewStore()

    var body: some View {
        VStack(alignment: .leading) {
            contentSection
            Divider()
            urlInputSection
        }
        .padding()
        .background(Color(red: 0.15, green: 0.15, blue: 0.15))
    }

    private var contentSection: some View {
        Group {
            if webViewStore.isLoading {
                loadingView
            } else {
                VStack {
                    HStack(alignment: .top) {
                        if let uiImage = webViewStore.fetchedUIImage {
                            Image(uiImage: uiImage)
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(width: 100, height: 100)
                                .cornerRadius(5)
                        }

                        VStack(alignment: .leading) {
                            userProfileSection
                            descriptionSection
                            Spacer()
                        }
                        .padding(.leading, 10)
                    }

                    Spacer()
                }
            }
        }
    }

    private var userProfileSection: some View {
        HStack {
            if let userProfileImage = webViewStore.fetchedProfileUIImage {
                Image(uiImage: userProfileImage)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width: 100, height: 100)
            }

            if let username = webViewStore.fetchedUsername {
                Text(username)
                    .font(.caption)
                    .foregroundColor(.white)
            }
        }
    }

    private var descriptionSection: some View {
        Group {
            if let description = webViewStore.fetchedDescription {
                Text(description)
                    .font(.caption)
                    .foregroundColor(.white)
                    .lineLimit(4)
                    .padding(.top, 5)
            }
        }
    }

    private var urlInputSection: some View {
        HStack {
            Button(action: {
                webViewStore.loadURL(url: URL(string: "https://www.instagram.com/p/CxGEZGWrH-N/")!)
            }) {
                Text("Load")
                    .foregroundColor(.white)
                    .padding(.horizontal, 16)
                    .padding(.vertical, 8)
                    .background(Color.blue)
                    .cornerRadius(8)
            }
            .frame(height: 36)
        }
    }

    private var loadingView: some View {
        VStack {
            ProgressView("Loading...")
                .padding()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color(red: 0.15, green: 0.15, blue: 0.15))
    }
}

class WebViewStore: NSObject, ObservableObject, WKNavigationDelegate {
    @Published var fetchedUIImage: UIImage?
    @Published var fetchedDescription: String?
    @Published var fetchedProfileUIImage: UIImage?
    @Published var fetchedUsername: String?
    @Published var isLoading: Bool = false
    
    // 新增:跟踪图片加载状态
    private var totalImagesToLoad = 2
    private var loadedImageCount = 0
    
    private var webView: WKWebView

    override init() {
        let webConfiguration = WKWebViewConfiguration()
        self.webView = WKWebView(frame: .zero, configuration: webConfiguration)
        super.init()
        self.webView.navigationDelegate = self
    }

    func loadURL(url: URL) {
        isLoading = true
        // 重置加载状态和旧数据
        loadedImageCount = 0
        fetchedUIImage = nil
        fetchedProfileUIImage = nil
        
        let request = URLRequest(url: url)
        webView.load(request)
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        fetchContent()
        attemptFetchingDescription(retryCount: 3)
    }
    
    private func fetchContent() {
        let fetchImageScript = """
        (function() {
            var imageElement = document.querySelector('meta[property="og:image"]');
            var result = {
                imageURL: null,
            };
            if (imageElement) {
                result.imageURL = imageElement.getAttribute('content');
            }
            return result;
        })();
        """

        webView.evaluateJavaScript(fetchImageScript) { (fetchResult, fetchError) in
            if let result = fetchResult as? [String: String], let imageURLString = result["imageURL"], let validURL = URL(string: imageURLString) {
                self.loadImage(from: validURL, assignTo: \.fetchedUIImage)
            } else {
                // 如果图片获取失败,也算完成一次加载,避免卡住
                self.markImageLoadingComplete()
            }
        }
    }
    
    private func loadImage(from url: URL, assignTo property: ReferenceWritableKeyPath<WebViewStore, UIImage?>) {
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            DispatchQueue.main.async {
                if let imageData = data, let uiImage = UIImage(data: imageData) {
                    self[keyPath: property] = uiImage
                }
                // 不管加载成功还是失败,都标记完成
                self.markImageLoadingComplete()
            }
        }.resume()
    }
    
    // 新增:标记图片加载完成并检查是否全部完成
    private func markImageLoadingComplete() {
        loadedImageCount += 1
        if loadedImageCount >= totalImagesToLoad {
            isLoading = false
        }
    }

    private func attemptFetchingDescription(retryCount: Int) {
        if retryCount <= 0 {
            // 重试失败,强制关闭加载动画
            isLoading = false
            return
        }

        let checkScript = "document.body.innerHTML.includes('Instagram')"

        DispatchQueue.main.async {
            self.webView.evaluateJavaScript(checkScript) { (result, error) in
                if let isPresent = result as? Bool, isPresent {
                    self.fetchImageURL(retryCount: retryCount)
                } else {
                    DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
                        self.attemptFetchingDescription(retryCount: retryCount - 1)
                    }
                }
            }
        }
    }

    private func fetchImageURL(retryCount: Int) {
        let fetchImageScript = "var img = document.querySelector('header img.xpdipgo.x972fbf'); img ? img.getAttribute('src') : 'Not found';"
        DispatchQueue.main.async {
            self.webView.evaluateJavaScript(fetchImageScript) { (fetchResult, fetchError) in
                if let link = fetchResult as? String, !link.contains("Not found"), let validURL = URL(string: link) {
                    self.loadImage(from: validURL, assignTo: \.fetchedProfileUIImage)
                } else {
                    if retryCount > 1 {
                        DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
                            self.attemptFetchingDescription(retryCount: retryCount - 1)
                        }
                    } else {
                        // 最后一次重试失败,标记完成
                        self.markImageLoadingComplete()
                    }
                }
            }
        }
    }
}

关键改动说明

  • 状态跟踪变量:totalImagesToLoad设为2(对应两张图片),loadedImageCount记录已完成的数量。
  • 加载初始化:调用loadURL时重置计数器和图片数据,确保每次加载都是全新状态。
  • 图片加载完成逻辑:新增markImageLoadingComplete方法,每次图片加载(成功或失败)后递增计数,当计数达到总数时关闭加载动画。
  • 错误处理:如果图片获取失败或重试耗尽,都会标记加载完成,避免加载动画一直显示。

内容的提问来源于stack exchange,提问作者P_n

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:25:55