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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 19:47:41