I'm having trouble making async functions run in background threads (to prevent blocking the main thread).
Below is a method that takes about 5 seconds to run.
From what I've learned, it seemed like making the function async and marking it with await on function call would be enough. But it doesn't work as intended and still freezes up the UI.
EDIT
Since it's stated that Swift 5.5 concurrency can replace DispatchQueue, I am trying to find a way to do this with only Async/Await.
EDIT_2
I did try removing the #MainActor wrapper, but it still seem to run on the main thread.
NumberManager.swift
#MainActor class NumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
// takes about 5 seconds to run...
} }
ContentView
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View{
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
.onAppear() {
Task {
// Runs in the main thread, freezing up the UI until it completes.
await numberManager.generateNumbers()
}
}
}
.tabItem {
Label("One", systemImage: "list.dash")
}
Text("Hello")
.tabItem {
Label("Two", systemImage: "square.and.pencil")
}
}
}
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
} }
What I've tried...
I've tried a few things, but the only way that made it run in the background was changing the function as below. But I know that using Task.detached should be avoided unless it's absolutely necessary, and I didn't think this is the correct use-case.
func generateNumbers() async {
Task.detached {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
await MainActor.run { [numbers] in
self.numbers = numbers
}
}
Writing async on a function doesn’t make it leave the thread. You need a continuation and you need to actually leave the thread somehow.
Some ways you can leave the thread using DispatchQueue.global(qos: .background).async { or use Task.detached.
But the most important part is returning to the main thread or even more specific to the Actor's thread.
DispatchQueue.main.async is the "old" way of returning to the main thread it shouldn't be used with async await. Apple as provided CheckedContinuation and UncheckedContinuation for this purpose.
Meet async/await can elaborate some more.
import SwiftUI
struct ConcurrentSampleView: View {
//Solution
#StateObject var vm: AsyncNumberManager = .init()
//Just to create a project that can show both scenarios.
//#StateObject var vm: NumberManager = .init()
#State var isLoading: Bool = false
var body: some View {
HStack{
//Just to visualize the thread being released
//If you use NumberManager the ProgressView won't appear
//If you use AsyncNumberManager the ProgressView WILL appear
if isLoading{
ProgressView()
}
Text(vm.numbers == nil ? "nil" : "\(vm.numbers?.count.description ?? "")")
}
//.task is better for iOS 15+
.onAppear() {
Task{
isLoading = true
await vm.generateNumbers()
isLoading = false
}
}
}
}
struct ConcurrentSampleView_Previews: PreviewProvider {
static var previews: some View {
ConcurrentSampleView()
}
}
#MainActor
class AsyncNumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
numbers = await concurrentGenerateNumbers()
}
private func concurrentGenerateNumbers() async -> [Double] {
typealias Cont = CheckedContinuation<[Double], Never>
return await withCheckedContinuation { (cont: Cont) in
// This is the asynchronous part, have the operation leave the current actor's thread.
//Change the priority as needed
//https://developer.apple.com/documentation/swift/taskpriority
Task.detached(priority: .utility){
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
//This tells the function to return to the actor's thread
cont.resume(returning: numbers)
}
}
}
//Or something like this it just depends on the true scenario
private func concurrentGenerateNumbers2() async -> [Double] {
// This is the asynchronous part, have the operation leave the actor's thread
//Change the priority as needed
//https://developer.apple.com/documentation/swift/taskpriority
return await Task.detached(priority: .utility){
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
return numbers
}.value
}
}
//Incorrect way of applying async/await. This doesn't actually leave the thread or mark when to return. Left here to highlight both scenarios in a reproducible example.
#MainActor
class NumberManager: ObservableObject {
#Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
}
}
You have answered your own question - You can use structured concurrency to solve your problem.
Your problem occurs because you have use the #MainActor decorator on your class. This means that it executes on the main queue.
You can either remove this decorator or, as you have found, use structured concurrency to explicitly create a detached task and the use a main queue task to provide your result.
Which approach you use depends on what else this class does. If it does a lot of other work that needs to be on the main queue then #MainActor is probably a good approach. If not then remove it.
The #MainActor property wrapper is not (just) what makes your observable object run on the main thread. #StateObject is what's doing it. Which is logical, since changes to the object will update the UI.
Removing the #MainActor wrapper is not the solution, because any changes to #Published properties will have to be done on the main thread (since they update the UI). You also don't want to run Task.detached, at least not if the task is going to change any #Published property, for the same reason.
By marking your generateNumbers function as async, a method from the main thread can call it with await - which allows the task to suspend and not block the main thread. That's what makes it concurrent.
extension NumberManager {
func loadNumbers() {
// This task will run on the main thread
Task {
// `await` tells the Swift executor that this method can run in the background,
// and the main thread can continue doing other things while it waits for its result
self.numbers = await self.generateNumbers()
}
}
func generateNumbers() async -> [Double] {
return (1...10_000_000).map { _ in Double.random(in: -10...10)
}
}
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View {
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
}
}
.onAppear { numberManager.loadNumbers() }
}
}
A more complete loadNumbers method could also store a reference to the task allowing you to cancel or restart a running task. However nowadays we have the excellent .task(priority:_:) to do all of that for you. It manages the task's lifecycle automatically, which means less boilerplate code.
struct ContentView: View {
#StateObject private var numberManager = NumberManager()
var body: some View {
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
}
}
// generate numbers
.task(.priority: .high) {
numberManager.numbers = await numberManager.generateNumbers
}
}
}
As far as I know this is the most succinct way to do expensive calculations on a background thread in Swift 5.7.
Related
I need to filter my model data with a search bar. I added the .searchable() property and when the search text changes I filter my objects with fuzzy matching. This takes too much time and the app lags when writing into the search box. So I want to do the searching asynchronously so that the app doesn't freeze.
I tried to do it with the onChange(of:) property and then I create a Task that runs the async function because the onChange() property doesn't allow async functions by themselves. But the app still lags.
Here is a code example of how I tried doing it:
import SwiftUI
import Fuse
struct SearchView: View {
#EnvironmentObject var modelData: ModelData
#State var searchText = ""
#State var searchResults: [Item] = []
#State var searchTask: Task<(), Never>? = nil
let fuseSearch = Fuse()
var body: some View {
// Show search results
}
.searchable(text: $searchText)
.onChange(of: searchText) { newQuery in
// Cancel if still searching
searchTask?.cancel()
searchTask = Task {
searchResults = await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
}
func fuzzyMatch(items: [Item], searchText: String) async -> [Item] {
filteredItems = items.filter {
(fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.25
}
return filteredItems
}
}
I would really appreciate some help.
I think the main problem is debouncing as lorem ipsum mentioned before.
I just tested my code and you need to call your filter method where i printed.
In this way you will not filter for every editing textfield. You will filter after some millisecond which you may change.
You can find more detail in this link
SwiftUI Combine Debounce TextField
struct Example: View {
#State var searchText = ""
let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
NavigationView {
Text("Test")
}
.searchable(text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
print("call your filter method")
}
}
}
If you want to introduce debouncing, you can just add a Task.sleep:
.onChange(of: searchText) { newQuery in
// Cancel if still searching
searchTask?.cancel()
searchTask = Task {
try await Task.sleep(for .seconds(0.5))
searchResults = await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
}
If you do that, you will have to change searchTask to be Task<(), Error>?.
However, debouncing might not be the whole issue. If your fuzzy filtering is too slow, you might need to make it run asynchronously and get it off the main thread:
The fuzzyMatch is marked as async, but is not currently doing anything asynchronous. Having the method signature reflect what is going on may make it easier to reason about one’s code.
To that end, now that we realize that fuzzyMatch runs synchronously, it becomes apparent that if it runs slowly, it will block the current thread. And because Task { ... } runs on the current actor, you will end up blocking the main thread. You should consider using Task.detached to get it on a background thread. But mark searchResults as being on the main actor.
If you cancel the Task, it will not stop fuzzyMatch that is underway. It should check for cancelation.
So, pulling that all together, perhaps:
struct ContentView: View {
#StateObject var modelData: ModelData()
#State var searchText = ""
#MainActor #State var searchResults: [Item] = []
#State var searchTask: Task<[Item], Error>?
let fuseSearch = Fuse()
var body: some View {
NavigationStack {
...
}
.searchable(text: $searchText)
.onChange(of: searchText) { newQuery in
Task {
searchTask?.cancel()
let task = Task.detached {
try await Task.sleep(for: .seconds(0.5)) // debounce; if you don't want debouncing, remove this, but it can eliminate annoying updates of the UI while the user is typing
return try await fuzzyMatch(items: modelData.items, searchText: newQuery)
}
searchTask = task
searchResults = try await task.value
}
}
}
func fuzzyMatch(items: [Item], searchText: String) throws -> [Item] {
try items.filter {
try Task.checkCancellation()
return (fuseSearch.search(searchText, in: $0.name)?.score ?? 1) < 0.25
}
}
}
I have a sample program that does three things
Generate a random integer from -10...10 (regular function)
Generate 10 million random numbers from -10...10 (asynchronous function)
Calculate the average of #2 (throwing asynchronous function)
Below is the full working code. It works without errors, but the view has a horrible readability with three nested if/let loops. What's the best way/convention to get rid of the pyramid of doom in this scenario?
Result screenshot (how it should work)
Working code (methods)
class NumberManager: ObservableObject {
#Published var integer: Int?
#Published var numbers: [Double]?
#Published var average: Double?
func generateInt() {
self.integer = Int.random(in: -10...10)
}
func generateNumbers() async {
self.numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
// takes about 5 seconds to run...
}
func calculateAverageNumber(for numbers: [Double]) async throws {
guard !numbers.isEmpty else {
print("numbers not generated")
return
}
let total = numbers.reduce(0, +)
let average = total / Double(numbers.count)
self.average = average
}
}
Working code (view)
struct ContentView: View {
#StateObject var numberManager = NumberManager()
var body: some View {
if let integer = numberManager.integer {
if let numbers = numberManager.numbers {
if let average = numberManager.average {
Text("Integer is \(integer)")
Text("First number is: \(numbers[0])")
Text("Average is: \(average)")
} else {
LoadingView(loadingType: "Calculating average")
.task {
do {
try await numberManager.calculateAverageNumber(for: numbers)
} catch {
print("empty numbers array")
}
}
}
} else {
LoadingView(loadingType: "Generating numbers")
.task {
await numberManager.generateNumbers()
}
}
} else {
LoadingView(loadingType: "Generating int")
.task {
numberManager.generateInt()
}
}
}
}
What I tried so far...
I tried building helper functions to build views as below, and called those functions that returns views inside my ContentView. When I run it, the integer and the number array gets generated and shows, but the last task that calculates the average does not get called again at all.
Result screenshot(with issues)
Code (Runs without errors. But the last task that calculates average doesn't get executed)
struct ContentView: View {
#StateObject var numberManager = NumberManager()
var body: some View {
intergerView()
.task {
print("Generating Int")
numberManager.generateInt()
}
numbersView()
.task {
print("Generating Numbers")
await numberManager.generateNumbers()
}
averageView()
.task {
do {
print("Calculating Average")
try await numberManager.calculateAverageNumber(for: numberManager.numbers ?? [])
} catch {
print("error")
}
}
}
}
private func intergerView() -> some View {
guard let integer = numberManager.integer else {
return AnyView(LoadingView(loadingType: "Generating int"))
}
return AnyView(Text("Integer is \(integer)"))
}
private func numbersView() -> some View {
guard let numbers = numberManager.numbers else {
return AnyView(LoadingView(loadingType: "Generating numbers"))
}
return AnyView(Text("First number is: \(numbers[0])"))
}
private func averageView() -> some View {
guard let average = numberManager.average else {
return AnyView(LoadingView(loadingType: "Calculating average"))
}
return AnyView(Text("Average is: \(average)"))
}
EDIT: In my app, I have a view that does all different functions in one view (it's like a dashboard). Some require others to run first (like calculating the average), whereas some can run on its own (Like generating one random integer). I want to display whatever that's loaded first, while displaying a loadingview placeholder for parts that aren't loaded yet.
Several issues here:
generateNumbers and calculateAverageNumber depend on each other. So they need to await each other.
your "working code" does not match the description of your code. You say you want to show what ever finishes first but your if/else statements introduce dependencies between all 3 functions/views
you don´t need 3 different views. One that can be customized should be enough.
class NumberManager: ObservableObject {
#Published var integer: Int?
#Published var numbers: [Double]?
#Published var average: Double?
func generateInt() {
self.integer = Int.random(in: -10...10)
}
func generateNumbers() async {
self.numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
// takes about 5 seconds to run...
}
// No need for arguments here
func calculateAverageNumber() async throws {
guard let numbers = numbers, !numbers.isEmpty else {
print("numbers not generated")
return
}
let total = numbers.reduce(0, +)
let average = total / Double(numbers.count)
self.average = average
}
//This function will handle the dependenies of generating the values and calculating the avarage
func calculateNumbersAndAvarage() async throws{
await generateNumbers()
try await calculateAverageNumber()
}
}
The View:
struct ContentView: View{
#StateObject private var numberManager = NumberManager()
var body: some View{
//Show the different detail views.
VStack{
Spacer()
Spacer()
DetailView(text: numberManager.integer != nil ? "Integer is \(numberManager.integer!)" : nil)
.onAppear {
numberManager.generateInt()
}
Spacer()
Group{
DetailView(text: isNumbersValid ? "First number is: \(numberManager.numbers![0])" : nil)
Spacer()
DetailView(text: numberManager.average != nil ? "Average is: \(numberManager.average!)" : nil)
}.onAppear {
Task{
do{
try await numberManager.calculateNumbersAndAvarage()
}
catch{
print("error")
}
}
}
Spacer()
Spacer()
}
}
//Just to make it more readable
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
}
}
and the DetailView:
struct DetailView: View{
let text: String?
var body: some View{
// If no text to show, show `ProgressView`, or `LoadingView` in your case. You can inject the view directly or use a property for the String argument.
if let text = text {
Text(text)
.font(.headline)
.padding()
} else{
ProgressView()
}
}
}
The code should speak for itself. If you have any further question regarding this code please feel free to do so, but please read and try to understand how this works first.
Edit:
This does not wait for calculateAverageNumber to finish before displaying numbers[0]. The reason for it showing at the same time is that it takes almost no time to calculat the avarage. Try adding this between the 2 functions in calculateNumbersAndAvarage.
try await Task.sleep(nanoseconds: 4_000_000_000)
and you will see that it shows as it should.
You are almost done. Use #ViewBuilder , remove AnyView wrapper and dont use guard
#ViewBuilder
var intergerView: some View {
if let integer = numberManager.integer {
LoadingView(loadingType: "Generating int")
} else {
Text("Integer is \(integer)")
}
}
Found this issue while working with the new Swift concurrency tools.
Here's the setup:
class FailedDeinit {
init() {
print(#function, id)
task = Task {
await subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
var instance: FailedDeinit? = FailedDeinit()
instance = nil
Running this code in a Playground yields this:
init() F007863C-9187-4591-A4F4-BC6BC990A935
!!! The deinit method is never called!!!
Strangely, when I change the code to this:
class SuccessDeinit {
init() {
print(#function, id)
task = Task {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
}
deinit {
print(#function, id)
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
var instance: SuccessDeinit? = SuccessDeinit()
instance = nil
By moving the code from the method subscribe() directly in the Task, the result in the console changes to this:
init() 0C455201-89AE-4D7A-90F8-D6B2D93493B1
deinit 0C455201-89AE-4D7A-90F8-D6B2D93493B1
This may be a bug or not but there is definitely something that I do not understand. I would welcome any insight about that.
~!~!~!~!
This is crazy (or maybe I am?) but with a SwiftUI macOS project. I still DON'T get the same behaviour as you. Look at that code where I kept the same definition of the FailedDeinit and SuccessDeinit classes but used them within a SwiftUI view.
struct ContentView: View {
#State private var failed: FailedDeinit?
#State private var success: SuccessDeinit?
var body: some View {
VStack {
HStack {
Button("Add failed") { failed = .init() }
Button("Remove failed") { failed = nil }
}
HStack {
Button("Add Success") { success = .init() }
Button("Remove Success") { success = nil }
}
}
}
}
class FailedDeinit {
init() {
print(#function, id)
task = Task { [weak self] in
await self?.subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
Consider the following:
task = Task {
await subscribe()
}
It is true that introduces a strong reference to self. You can resolve that strong reference with:
task = Task { [weak self] in
await self?.subscribe()
}
But that is only part of the problem here. This [weak self] pattern only helps us in this case if either the Task has not yet started or if it has finished.
The issue is that as soon as subscribe starts executing, despite the weak reference in the closure, it will keep a strong reference to self until subscribe finishes. So, this weak reference is prudent, but it is not the whole story.
The issue here is more subtle than appears at first glance. Consider the following:
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
The subscribe method will keep executing until the stream calls finish. But you never finish the stream. (You don’t yield any values, either. Lol.) Anyway, without anything in the AsyncStream, once subscribe starts it will never complete and thus will never release self.
So let us consider your second rendition, when you create the Task, bypassing subscribe:
task = Task {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
Yes, you will see the object be deallocated, but you are neglecting to notice that this Task will never finish, either! So, do not be lulled into into a false sense of security just because the containing object was released: The Task never finishes! The memory associated with that Task will never get released (even if the parent object, FailedDeinit in your example, is).
This all can be illustrated by changing your stream to actually yield values and eventually finish:
task = Task {
let stream = AsyncStream<Double> { continuation in
Task {
for i in 0 ..< 10 {
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SECOND)
continuation.yield(Double(i))
}
continuation.finish()
}
}
for await p in stream {
print("\(p)")
}
print("all done")
}
In this case, if you dismiss it while the stream is underway, you will see that the AsyncStream continues until it finishes. (And, if you happen to be doing this inside a method, the object in question will also be retained until the task is canceled.)
So, what you need to do is to cancel the Task if you want the AsyncStream to finish. And you also should implement onTermination of the continuation in such a manner that it stops the asynchronous stream.
But, the result is that if I cancel this when the view controller (or whatever) is released, then my example yielding values 0 through 9 will stop and the task will be freed.
It all comes down to what your AsyncStream is really doing. But in the process of simplifying the MCVE and removing the contents of the AsyncStream, you simultaneously do not handle cancelation and never call finish. Those two, combined, manifest the problem you describe.
This doesn't really have anything to do with async/await or AsyncStream. It's a perfectly normal retain cycle. You (the FailedDeinit instance) are retaining the task, but the task refers to subscribe which is a method of you, i.e. self, so the task is retaining you. So simply break the retain cycle just like you would break any other retain cycle. Just change
task = Task {
await subscribe()
}
To
task = Task { [weak self] in
await self?.subscribe()
}
Also, be sure to test in a real project, not a playground, as playgrounds are not indicative of anything in this regard. Here's the code I used:
import UIKit
class FailedDeinit {
init() {
print(#function, id)
task = Task { [weak self] in
await self?.subscribe()
}
}
deinit {
print(#function, id)
}
func subscribe() async {
let stream = AsyncStream<Double> { _ in }
for await p in stream {
print("\(p)")
}
}
private var task: Task<(), Swift.Error>?
let id = UUID()
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
var instance: FailedDeinit? = FailedDeinit()
instance = nil
}
}
I have a Complex class which I pass around as an EnvironmentObject through my SwiftUI views. Complex contains several CurrentValueSubjects. I don't want to add the Published attribute to the publishers on class Complex, since Complex is used a lot around the views and that will force the views to reload on every published value.
Instead, I want a mechanism which can subscribe to specific publisher which Complex holds. That way, Views can choose on which publisher the view should re-render itself.
The code below works, but I was wondering if there was an easier solution, it feels like a lot of work just to listen to the updates CurrentValueSubject gives me:
import SwiftUI
import Combine
struct ContentView: View {
let complex = Complex()
var body: some View {
PublisherView(boolPublisher: .init(publisher: complex.boolPublisher))
.environmentObject(complex)
}
}
struct PublisherView: View {
#EnvironmentObject var complex: Complex
#ObservedObject var boolPublisher: BoolPublisher
var body: some View {
Text("\(String(describing: boolPublisher.publisher))")
}
}
class Complex: ObservableObject {
let boolPublisher: CurrentValueSubject<Bool, Never> = .init(true)
// A lot more...
init() {
startToggling()
}
func startToggling() {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [unowned self] in
let newValue = !boolPublisher.value
print("toggling to \(newValue)")
boolPublisher.send(newValue)
startToggling()
}
}
}
class BoolPublisher: ObservableObject {
private var cancellableBag: AnyCancellable? = nil
#Published var publisher: Bool
init(publisher: CurrentValueSubject<Bool, Never>) {
self.publisher = publisher.value
cancellableBag = publisher
.receive(on: DispatchQueue.main)
.sink { [weak self] value in
self?.publisher = value
}
}
}
I previously asked a question about how to push a view with data received from an asynchronous callback. The method I ended up with has turned out to cause a Memory Leak.
I'm trying to structure my app with MVVM for SwiftUI, so a ViewModel should publish another ViewModel, that a View then knows how to present on screen. Once the presented view is dismissed from screen, I expect the corresponding ViewModel to be deinitialised. However, that's never the case with the proposed solution.
After UserView is dismissed, I end up having an instance of UserViewModel leaked in memory. UserViewModel never prints "Deinit UserViewModel", at least not until next time a view is pushed on pushUser.
struct ParentView: View {
#ObservedObject var vm: ParentViewModel
var presentationBinding: Binding<Bool> {
.init(get: { vm.pushUser != nil },
set: { isPresented in
if !isPresented {
vm.pushUser = nil
}
}
)
}
var body: some View {
VStack {
Button("Get user") {
vm.getUser()
}
Button("Read user") {
print(vm.pushUser ?? "No userVm")
}
if let userVm = vm.pushUser {
NavigationLink(
destination: UserView(vm: userVm),
isActive: presentationBinding,
label: EmptyView.init
)
}
}
}
}
class ParentViewModel: ObservableObject {
#Published var pushUser: UserViewModel? = nil
var cancellable: AnyCancellable?
private func fetchUser() -> AnyPublisher<User, Never> {
Just(User.init(id: "1", name: "wiingaard"))
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
func getUser() {
cancellable = api.getUser().sink { [weak self] user in
self?.pushUser = UserViewModel(user: user)
}
}
}
struct User: Identifiable {
let id: String
let name: String
}
class UserViewModel: ObservableObject, Identifiable {
deinit { print("Deinit UserViewModel") }
#Published var user: User
init(user: User) { self.user = user }
}
struct UserView: View {
#ObservedObject var vm: UserViewModel
var body: some View {
Text(vm.user.name)
}
}
After dismissing the UserView and I inspect the Debug Memory Graph, I see an instance of UserViewModel still allocated.
The top reference (view.content.vm) has kind: (AnyViewStorage in $7fff57ab1a78)<ModifiedContent<UserView, (RelationshipModifier in $7fff57ad2760)<String>>> and hierarchy: SwiftUI.(AnyViewStorage in $7fff57ab1a78)<SwiftUI.ModifiedContent<MyApp.UserView, SwiftUI.(RelationshipModifier in $7fff57ad2760)<Swift.String>>> AnyViewStorageBase _TtCs12_SwiftObject
What's causing this memory leak, and how can I remove it?
I can see that ViewModel is deinit() if you use #State in your View, and listen to your #Publisher in your ViewModel.
Example:
#State var showTest = false
NavigationLink(destination: SessionView(sessionViewModel: outgoingCallViewModel.sessionViewModel),
isActive: $showTest,
label: { })
.isDetailLink(false)
)
.onReceive(viewModel.$showView, perform: { show in
if show {
showTest = true
}
})
if you use viewModel.$show in your NavigationLink as isActive, viewModel never deinit().
Please refer to this post (https://stackoverflow.com/a/62511130/11529487), it solved the issue for the memory leak bug in SwiftUI by adding on the NavigationView:
.navigationViewStyle(StackNavigationViewStyle())
However it breaks the animation, there is a hacky solution to this issue. The animation problem occurs because of the optional chaining in "if let".
When setting "nil" as the destination in the NavigationLink, it essentially does not go anywhere, even if the "presentationBinding" is true.
I invite you to try this piece of code as it fixed the animtaion problem that resulted from the StackNavigationViewStyle (and no memory leaks):
Although not as pretty as the optional chaining, it does the job.