SwiftUI加载动画仅在首张图片获取后停止,如何保持至全资源加载完成?
解决方案
问题出在每次单张图片加载完成后就直接将isLoading设为false,导致第一张图片加载完就关闭了加载动画。我们需要跟踪所有需要加载的资源状态,等全部完成后再隐藏加载动画。
具体修改步骤
- 添加加载状态跟踪变量:在
WebViewStore中新增两个变量,记录需要加载的图片总数和已完成加载的数量,同时重置旧的图片数据避免缓存干扰。 - 修改加载初始化逻辑:调用
loadURL时,初始化计数器并清空之前的图片数据。 - 更新图片加载完成后的逻辑:不再直接关闭加载动画,而是递增完成计数,当完成数等于总数时才关闭加载动画。
- 处理重试失败的边界情况:当重试次数耗尽时,强制关闭加载动画,避免无限加载。
修改后的完整代码
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
相关产品推荐
相关产品推荐

