SwiftUI ViewModel 未被 deinit 并导致内存泄漏

问题描述 投票:0回答:1

我正在 Xcode 上开发一个 Mac OS 应用程序,该应用程序从 API 中提取足球排名并将其显示在视图中。我注意到我的内存不断增加,并且在使用 Xcode Memory Graph 之后,我相信我的问题是 ViewModel 没有被 deinit,因此仍然被分配内存。例如下图中,从

BundesligaView
来回3次就创建了3个独立的模型。

下面的代码比我原来的应用程序简化了很多。当从 LeagueIndex -> BundesligaView 移动时,会创建一个 viewModel。我不确定是否应该在某个地方添加一个[弱自我]?

// VIEWMODEL
import Foundation

class BundesligaViewModel: ObservableObject {
    @Published var standings: [BundesligaModel] = []
    @Published var loadingState: LoadingState = .idle
    
    @MainActor
    func fetchData(_ league: String? = nil) async {
        loadingState = .loading
        do { 
            standings = try await NetworkManager.shared.fetchBundesligaStandings(league!)
            loadingState = .loaded
        } catch {
            print("Error fetching standings: \(error)")
            loadingState = .error
        }
    }
    
    init() {
        print("VIEWMODEL CREATED")
    }

    deinit {
        print("VIEWMODEL DEINT")
    }
}

enum LoadingState {
    case idle
    case loading
    case loaded
    case error
}
// MODEL
import Foundation

struct BundesligaModel: Codable {
    let teamName: String
    let shortName: String
    let points: Int
    let matches: Int
    let won: Int
    let draw: Int
    let lost: Int
    let goals: Int
    let opponentGoals: Int
    let goalDiff: Int
}
// VIEWS
struct LeagueIndex: View {
    var body: some View {
        NavigationStack {
            NavigationLink(destination: BundesligaTable(league: "Bundesliga")) {
                Text("Bundesliga")
            }
            .buttonStyle(PlainButtonStyle())
        }
        .navigationBarBackButtonHidden(true)
    }
}

struct Footer: View {
    var body: some View {
        NavigationLink(destination: LeagueIndex()) {
            Text("League Index").foregroundStyle(Color.red)
        }
        .buttonStyle(PlainButtonStyle())
    }
}

struct BundesligaTable: View {
    let league: String
    @StateObject private var viewModel = BundesligaViewModel()
    
    let columns = [
        GridItem(.fixed(25)),
        GridItem(.flexible()),
        GridItem(.fixed(25)),
        GridItem(.fixed(25)),
        GridItem(.fixed(25)),
        GridItem(.fixed(25)),
        GridItem(.fixed(25)),
        GridItem(.fixed(25)),
        GridItem(.fixed(25)),
        GridItem(.fixed(25))
    ]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 10) {
                ForEach(Array(viewModel.standings.enumerated()), id: \.element.teamName) { index, team in
                    Text("\(index + 1)")
                    Text(team.shortName)
                    Text("\(team.matches)")
                    Text("\(team.won)")
                    Text("\(team.draw)")
                    Text("\(team.lost)")
                    Text("\(team.goals)")
                    Text("\(team.opponentGoals)")
                    Text("\(team.goalDiff)")
                    Text("\(team.points)")
                }
            }
            Footer()
        }
        .padding(.horizontal)
        .task { 
            await viewModel.fetchData(league)
        }
    }
}
// NETWORK MANAGER

class NetworkManager {
    static let shared = NetworkManager()
    
    func fetchData<T: Decodable>(from endpoint: String) async throws -> T {
        guard let url = URL(string: endpoint) else {
            throw URLError(.badURL)
        }
        
        let (data, response) = try await URLSession.shared.data(from: url)
        
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw URLError(.badServerResponse)
        }
        
        do {
            let decoder = JSONDecoder()
            let decodedData = try decoder.decode(T.self, from: data)
            return decodedData
        } catch {
            throw error
        }
    }
}

extension NetworkManager {
    func fetchBundesligaStandings(_ league: String) async throws -> [BundesligaModel] {
        return try await fetchData(from: "https://api.openligadb.de/getbltable/bl1/2023")
    }
    // Other leagues here
}
ios swift xcode swiftui mvvm
1个回答
0
投票

发现问题了!我有一个自定义页脚,在

LeagueIndex
时导航到
BundesligaView
。它本应充当 NavigationStack 中的后退按钮,但它的实现错误,导致 View 和 ViewModel 被保留。

旧页脚:

struct Footer: View {
    var body: some View {
        HStack {
            NavigationLink(destination: LeagueIndex()) {
                Text("League Index").foregroundStyle(Color.red)
            }
        }
    }
}

新页脚:

struct Footer: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        HStack {
            Button(action: {
                dismiss()
            }) {
                HStack {
                    Text("League Index").foregroundStyle(Color.red)
                }
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.