Reputation: 26385
I've found this weird behavior and I cannot explain myself why is happening and how can I get rid of it.
Consider this code I tried to make a smaller MVP:
enum Sheet: Int, Identifiable {
var id: Int {
self.rawValue
}
case detail
case tutorial
}
struct HostingView: View {
@State var isPresenting: Bool = false
var body: some View {
Button("Press to present") {
isPresenting.toggle()
}
.fullScreenCover(isPresented: $isPresenting) {
MyModelView()
}
}
}
struct MyModelView: View {
@StateObject var model = Model()
@Environment(\.dismiss) private var dismiss
@State private var presentedSheet: Sheet?
@State private var currentIndex: Int = 0
@State private var messageList = [Message]()
var body: some View {
VStack {
ForEach(messageList) { message in
MessageView(with:message)
}
Button("Dismiss") {
dismiss()
}
}
.task {
await model.loadData()
self.messageList = buildMessages(with: model.data)
}.sheet(item: $presentedSheet) { sheet in
switch sheet {
case .detail:
Text("Detail")
case .tutorial:
Text("Tutorial")
}
}
}
private func buildMessages(with data: [Int]) -> [Message] {
[
Message(text: "Interesting message \(data[0])"),
Message(text: "Interesting message with action \(data[1])", buttonAction: {
presentedSheet = .tutorial // HERE THE REF CYCLE
})
]
}
}
struct Message: Identifiable {
var id = UUID()
let text: String
let buttonAction: (() -> Void)?
init(text: String,
buttonAction: (() -> Void)? = nil) {
self.text = text
self.buttonAction = buttonAction
}
}
struct MessageView: View {
let text: String
let buttonAction: (() -> Void)?
init(with message: Message) {
self.text = message.text
self.buttonAction = message.buttonAction
}
var body: some View {
VStack(alignment: .leading) {
Text(text)
if let buttonAction {
Button(action: {
buttonAction()
}, label: {
Text("Press Here")
})
}
}
.frame(maxWidth: .infinity,
alignment: .leading)
.background(Color.red)
}
}
class Model: ObservableObject {
@Published var data: [Int] = []
func loadData() async {
try? await Task.sleep(nanoseconds: 1_000_000)
await MainActor.run {
self.data = [1,2,3,4]
}
}
}
I have a view that is presented modally fullscreen, this view instantiate a StateObject
, using a .task
modifier once it appears it starts to download data asking to the model to fetch them.
After the model has download those info it builds some Message
model that have to be displayed in the view.
Some of those messages have a callback for a button buttonAction
, that pressed have to display a specific sheet.
The callback is inject by directly accessing the @State
variable presentedSheet
to trigger the correct action.
This message array is then looped in a ForEach
to build MessageView
.
Looking at the memory graph I saw that the Model
instance is not deallocates after dismissing the view. A@StateObject
should be deallocated when the view that creates it disappear, but is not happening. The Model
instance is retained by the Message
buttonAction
closure with presentedSheet = .tutorial
and never released.
if I comment that part of code I can see that the Model
is dealloced correctly.
Since they are all structs I cannot weaken the reference, second why is happening should be self eventually passed by copy? How can I get rid of this ref cycle?
Upvotes: 0
Views: 58
Reputation: 36304
Currently you create a new model, @StateObject var model = Model()
that has no relations to any other, every time you display the MyModelView()
.
Try this approach using a single source of truth for your data model, declared
in the HostingView
and passed to other views using .environmentObject(model)
as shown in the example code:
struct HostingView: View {
@StateObject private var model = Model() // <--- here
@State private var isPresenting: Bool = false
var body: some View {
Button("Press to present") {
isPresenting.toggle()
}
.fullScreenCover(isPresented: $isPresenting) {
MyModelView()
.environmentObject(model) // <--- here
}
}
}
struct MyModelView: View {
@EnvironmentObject var model: Model // <--- here
// .....
See also Monitoring data it gives you some good examples of how to manage data in your app.
Upvotes: 0