Recently I noticed how my ViewModels aren't deallocating and they end up as a memory leaks. I found something similar in this thread but this is also happening without using @Observation. Check the source code below:
class CellViewModel: Identifiable {
let id = UUID()
var color: Color = Color.red
init() { print("init") }
deinit { print("deinit") }
}
struct CellView: View {
let viewModel: CellViewModel
var body: some View {
ZStack {
Color(viewModel.color)
Text(viewModel.id.uuidString)
}
}
}
@main
struct LeakApp: App {
@State var list = [CellViewModel]()
var body: some Scene {
WindowGroup {
Button("Add") {
list.append(CellViewModel())
}
Button("Remove") {
list = list.dropLast()
}
ScrollView {
LazyVStack {
ForEach(list) { model in
CellView(viewModel: model)
}
}
}
}
}
}
When I tap the Add button twice in the console I will see "init" message twice. So far so good. But then I click the Remove button twice and I don't see any "deinit" messages.
I used the Debug Memory Graph in Xcode and it showed me that two CellViewModel objects are in the memory and they are owned by the CellView and some other objects that I don't know where are they coming from (I assume from SwiftUI internally).
I tried using VStack instead of LazyVStack and that did worked a bit better but still not 100% "deinits" were in the Console.
I tried using weak var
struct CellView: View {
weak var viewModel: CellViewModel?
....
}
but this also helped only partially.
The only way to fully fix this is to have a separate class that holds the list of items and to use weak var viewModel: CellViewModel?. Something like this:
class CellViewModel: Identifiable {
let id = UUID()
var color: Color = Color.red
init() { print("init") }
deinit { print("deinit") }
}
struct CellView: View {
var viewModel: CellViewModel?
var body: some View {
ZStack {
if let viewModel = viewModel {
Color(viewModel.color)
Text(viewModel.id.uuidString)
}
}
}
}
@Observable
class ListViewModel {
var list = [CellViewModel]()
func insert() {
list.append(CellViewModel())
}
func drop() {
list = list.dropLast()
}
}
@main
struct LeakApp: App {
@State var viewModel = ListViewModel()
var body: some Scene {
WindowGroup {
Button("Add") {
viewModel.insert()
}
Button("Remove") {
viewModel.drop()
}
ScrollView {
LazyVStack {
ForEach(viewModel.list) { model in
CellView(viewModel: model)
}
}
}
}
}
}
But this won't work if I want to use @Bindable such as
@Bindable var viewModel: CellViewModel?
I don't understand why SwiftUI doesn't want to release the objects?
Post
Replies
Boosts
Views
Activity
Hi,
I am using StoreKit2 for managing subscriptions and while testing it works fine until the point when the subscription needs to auto-renew.
When using StoreKit Testing Configuration and running the app through Xcode the auto-renew process works fine.
On the other hand, when I disable StoreKit Testing Configuration and I use Sandbox environment (again through Xcode) then after my first period expires (1 month aka. 5 minutes) I keep getting
Product.SubscriptionInfo.RenewalInfo.ExpirationReason.autoRenewDisabled
This is a code block I use for loading latest subscription data
// Product is StoreKit.Product
for product in products {
do {
let statuses = try await product.subscription?.status
guard let statuses = statuses else { continue }
for status in statuses {
let statusRenewalInfo = try checkVerified(status.renewalInfo)
let statusTransaction = try checkVerified(status.transaction)
guard let expirationDate = statusTransaction.expirationDate else {
continue
}
// Here I am getting "autoRenewDisabled" status.
if expirationDate < Date() {
print("*** expiration reason: \(statusRenewalInfo.expirationReason?.description)")
}
}
}
}
StoreKit2 (aka. Transaction API) is fantastic because of it's async-await syntax but I experienced a lot of problems with it such as this one and the one where it doesn't refresh subscriptions history properly so any advice would be helpful.