SwiftUI中如何仅在视图首次显示时执行Task内的API调用?
SwiftUI 视图首次加载时仅调用一次API的正规实现
我已经实现了缓存机制,只需要在视图首次显示时调用API,但现在发现每次导航返回该视图时,API调用都会重复触发。我可以用if判断缓存来规避,但觉得这是临时方案。作为SwiftUI新手,想找到类似UIKit中viewDidLoad的正规实现方式。
相关代码如下:
final class ViewModel: ObservableObject { private let apiManager: APIManager @Published var image: UIImage? = nil init(apiManager: APIManager = APIManager()) { self.apiManager = apiManager } func fetchEmojis() async { do { _ = try await apiManager.downloadAsyncAwait(.emojis, type: emojis.self) } catch { print("API fetchEmojis issues: \(error)") } } } struct ContentView: View { @StateObject var viewModel = ViewModel() var body: some View { NavigationView { VStack { NavigationLink(destination: AvatarsGridView()) { Text("Avatar List") } Text("") .padding(0) } .task { print("start") await viewModel.fetchEmojis() print("finish") } } } }
APIManager代码(非核心):
typealias emojis = [String: String] enum EndpointEnum { private var baseURL: String { return "https://api.github.com" } case emojis case userAvatar(String) case appleRepositories(Int) private var fullPath: String { var endpoint:String switch self { case .emojis: endpoint = "/emojis" case .userAvatar(let userName): endpoint = "/users/\(userName)" case .appleRepositories(let page): endpoint = "/users/apple/repos?page=\(page)" } return baseURL + endpoint } var url: URL { guard let url = URL(string: fullPath) else { preconditionFailure("The url used in \(EndpointEnum.self) is not valid") } return url } } enum APIManagerError: Error { case httpStatus case decoding } final class APIManager { private let session: URLSession private let decoder = JSONDecoder() init(session: URLSession = URLSession.shared) { self.session = session self.coreDataManager = coreDataManager } func downloadAsyncAwait<T: Codable>(_ endpointEnum: EndpointEnum, type: T.Type) async throws -> T? { let url = endpointEnum.url let request = URLRequest(url: url) do { let (data, response) = try await session.data(for: request, delegate: nil) guard let response = response as? HTTPURLResponse, response.statusCode >= 200 && response.statusCode < 300 else { throw APIManagerError.httpStatus } let result = try decoder.decode(type, from: data) return result } catch { throw APIManagerError.decoding } } }
解决方案
SwiftUI中没有直接对应UIKit viewDidLoad的API,但可以通过以下几种正规方式实现仅首次加载时调用API:
方式一:利用@StateObject的初始化特性(推荐)
@StateObject修饰的ViewModel只会在视图首次创建时初始化一次,因此可以在ViewModel的初始化方法中启动异步任务,确保API调用仅执行一次。
修改后的ViewModel代码:
final class ViewModel: ObservableObject { private let apiManager: APIManager @Published var image: UIImage? = nil init(apiManager: APIManager = APIManager()) { self.apiManager = apiManager // 初始化时启动异步任务,仅执行一次 Task { await fetchEmojis() } } func fetchEmojis() async { do { _ = try await apiManager.downloadAsyncAwait(.emojis, type: emojis.self) } catch { print("API fetchEmojis issues: \(error)") } } }
对应的ContentView需移除.task修饰符:
struct ContentView: View { @StateObject var viewModel = ViewModel() var body: some View { NavigationView { VStack { NavigationLink(destination: AvatarsGridView()) { Text("Avatar List") } Text("") .padding(0) } } } }
方式二:给.task添加固定ID
如果希望保留视图层的任务触发逻辑,可以给.task传入一个固定不变的ID,任务只会在ID首次出现时执行,后续视图刷新或导航返回都不会重复触发。
修改后的ContentView:
struct ContentView: View { @StateObject var viewModel = ViewModel() var body: some View { NavigationView { VStack { NavigationLink(destination: AvatarsGridView()) { Text("Avatar List") } Text("") .padding(0) } // 固定ID确保任务仅执行一次 .task(id: "initial_emoji_fetch") { print("start") await viewModel.fetchEmojis() print("finish") } } } }
方式三:ViewModel内部管理加载状态
在ViewModel中添加标记记录是否已完成API调用,确保fetchEmojis方法仅执行一次,同时支持加载失败后的重试逻辑。
修改后的ViewModel:
final class ViewModel: ObservableObject { private let apiManager: APIManager @Published var image: UIImage? = nil // 标记是否已成功加载过数据 private var hasFetchedEmojis = false init(apiManager: APIManager = APIManager()) { self.apiManager = apiManager } func fetchEmojis() async { guard !hasFetchedEmojis else { return } hasFetchedEmojis = true do { _ = try await apiManager.downloadAsyncAwait(.emojis, type: emojis.self) } catch { print("API fetchEmojis issues: \(error)") // 加载失败时重置标记,允许后续重试 hasFetchedEmojis = false } } }
内容的提问来源于stack exchange,提问作者bruno
相关产品推荐
相关产品推荐

