Using Threads in Swift causes memory leak - ios

I encountered a strange behaviour in my application when using Threads inside my ViewModel. It seems something is happening in Thread.init which causes a memory leak even when the deinit of the Thread object is called (I'm not even starting the thread).
I'm relatively new to Swift so it might be my fault but I don't know why. I have two Views linked together via a NavigationView. The child View has a ViewModel which creates a Thread when the initialization method is called and destroys the Thread object when the deinitialization method is called. The deinitialization method of the ViewModel is called when the child View is "closed". After navigating back to the parent View I can see in the Memory Profiler of Xcode that there is a new memory leak
I post a small example which reproduces this issue below
The HomeView: (main view)
import SwiftUI
struct HomeView: View {
#State private var showView = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: TestView(showView: self.$showView), isActive : self.$showView) {
Text("Open MainView")
}.isDetailLink(false)
.navigationBarTitle("Home")
}
}
}
}
The TestView: (child view)
import SwiftUI
struct TestView: View {
#Binding var showView : Bool
#StateObject var mainViewModel = TestViewViewModel()
var body: some View {
Button("Back") {
showView = false
}.onAppear(perform: {
mainViewModel.initViewModel()
})
.onDisappear(perform: {
mainViewModel.deinitViewModel()
})
}
}
The ViewModel:
import Foundation
class TestViewViewModel: ObservableObject {
private var testThread: TestThread?
public func initViewModel() {
testThread = TestThread.init()
}
public func deinitViewModel() {
testThread?.cancel()
testThread = nil
}
/*+++++++++++++++*/
/*+ Test thread +*/
/*+++++++++++++++*/
private class TestThread: Thread {
override init(){
print("Init RenderThread")
}
deinit {
print("deinit RenderThread")
}
override func main() {
}
}
}
That's it, no magic involved but when clicking the Back button in the child view it seems something will not be garbage collected in the ViewModel's Thread. Any hints what might be wrong? Is this a bug or is there something I have to release manually?

Related

StateObject doesn't deinit when PhotosPicker was presented

I just do some experiments in my pet project on iOS 16 Beta. I noticed one possible memory leak. I created a simple project with simple steps which reproduce this behavior. So I have ContentView and DetailView I use NavigationStack for navigation. I created DetailViewModel for DetailView and add it as #StateObject and also add it as environmentObject for DetailView subviews. All good as expected but if I present/show PhotosPicker and return back to ContentView I can see that DetailViewModel isn't deinit. Is it memory leak? How can we fix/avoid this? Thanks.
Below I added code from the simple project also for convenience you can have a have look on GitHub
ContentView:
struct ContentView: View {
var body: some View {
NavigationStack {
let route = Route.detail
NavigationLink("Show Detail", value: route)
.navigationDestination(for: Route.self) { route in
switch route {
case .detail:
DetailView()
}
}
}
}
}
Route:
enum Route: Hashable {
case detail
}
DetailView:
import SwiftUI
import PhotosUI
struct DetailView: View {
#StateObject private var viewModel = DetailViewModel()
// just for test
#State private var photoPickerPresented = false
#State private var selectedPickerItem: PhotosPickerItem?
var body: some View {
VStack {
Button {
photoPickerPresented.toggle()
} label: {
Text("Show Photo Picker")
}
}
.photosPicker(isPresented: $photoPickerPresented, selection: $selectedPickerItem)
.environmentObject(viewModel)
}
}
DetailViewModel:
class DetailViewModel: ObservableObject {
init() {
debugPrint("DetailViewModel init")
}
deinit {
debugPrint("DetailViewModel deinit")
}
}

How do you edit an ObservableObject’s properties in SwiftUI from another class?

I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())

A View.environmentObject(_:) may be missing as an ancestor of this view - but not always…

I'm getting this error in production and can't find a way to reproduce it.
Fatal error > No ObservableObject of type PurchaseManager found. A
View.environmentObject(_:) for PurchaseManager may be missing as an
ancestor of this view. > PurchaseManager > SwiftUI
The crash comes from this view:
struct PaywallView: View {
#EnvironmentObject private var purchaseManager: PurchaseManager
var body: some View {
// Call to purchaseManager causing the crash
}
}
And this view is instantiated in subviews of the MainView
#main
struct MyApp: App {
let purchasesManager = PurchaseManager.shared
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(purchasesManager)
}
}
}
}
or, when called from a UIKit controller, from this controler:
final class PaywallHostingController: UIHostingController<AnyView> {
init() {
super.init(rootView:
AnyView(
PaywallView()
.environmentObject(PurchaseManager.shared)
)
)
}
#objc required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I tested all the use cases that trigger the PaywallView to show up, and I never got a crash.
FWIW, the PurchaseManager looks like this:
public class PurchaseManager: ObservableObject {
static let shared = PurchaseManager()
init() {
setupRevenueCat()
fetchOfferings()
refreshPurchaserInfo()
}
}
Why would the ObservableObject go missing? In which circumstances?
The reason your problem is intermittent, is probably because the PurchaseManager init()
could finish
before all the data is setup properly, due to the "delays" of the
async functions in init(). So sometimes the data will be available
when the View wants it, and sometimes it will not be there and crash your app.
You could try the following approach that includes #atultw advice of using
StateObject.
import SwiftUI
#main
struct TestApp: App {
#StateObject var purchaseManager = PurchaseManager() // <-- here
var body: some Scene {
WindowGroup {
MainView()
.onAppear {
purchaseManager.startMeUp() // <-- here
}
.environmentObject(purchaseManager)
}
}
}
struct MainView: View {
#EnvironmentObject var purchaseManager: PurchaseManager
var body: some View {
Text("testing")
List {
ForEach(purchaseManager.offerings, id: \.self) { offer in
Text(offer)
}
}
}
}
public class PurchaseManager: ObservableObject {
#Published var offerings: [String] = []
// -- here --
func startMeUp() {
// setupRevenueCat()
fetchOfferings()
// refreshPurchaserInfo()
}
func fetchOfferings() {
DispatchQueue.main.asyncAfter(deadline: .now()+2) {
self.offerings = ["offer 1","offer 2","offer 3","offer 4"]
}
}
}
Try not to use the singleton pattern here (.shared), EnvironmentObject is meant to be a replacement for it. You should instantiate PurchasesManager in MyApp.
#main
struct MyApp: App {
#StateObject var purchasesManager = PurchaseManager()
var body: some Scene {
WindowGroup {
MainView()
.environmentObject(purchasesManager)
}
}
}
}
without state object compiles fine but needed if you want child views to update automatically.
Doing those things with a dummy PurchasesManager runs fine for me.

SwiftUI Memory Leak With Forms and Lists

I am developing simple to-do app with SwiftUI. During my tests i noticed my view models never calls deinit and causing linear increase in memory usage.
I reproduced the same behavior with following code:
struct ContentView: View {
#State private var isPresented = false
var body: some View {
Button("open") {
self.isPresented = true
}
.sheet(isPresented: $isPresented) {
SheetView()
}
}
}
struct SheetView: View {
#ObservedObject var model: ViewModel
init() {
model = ViewModel()
}
var body: some View {
Form {
Toggle("Toggle Me", isOn: $model.isOn)
}
}
}
class ViewModel: ObservableObject {
#Published var isOn = false
deinit {
print("ViewModel deinit ")
}
}
When sheet is dismissed, model object never deinits. If i replace the form with VStack or ScrollView then model is deinited. Is there a solution to this?
It’s a bug. Only workaround that worked for me is using ScrollView. Then again, ScrollView comes with its own animation bugs.
EDIT
Issue seems to have been resolved in iOS 13.3.1
You're understanding deinit() wrong. When you're dismissing a View it doesn't necessarily mean it's going to call deinit() like you think. If your ViewModel was destroyed however it would call deinit() as you expect.
To demonstrate this, here’s a Person class with a name property, a simple initializer, and a printGreeting() method that prints a message:
class Person {
var name = "John Doe"
init() {
print("\(name) is alive!")
}
func printGreeting() {
print("Hello, I'm \(name)")
}
}
We’re going to create a few instances of the Person class inside a loop, because each time the loop goes around a new person will be created then destroyed:
for _ in 1...3 {
let person = Person()
person.printGreeting()
}
And now for the deinitializer. This will be called when the Person instance is being destroyed:
deinit {
print("\(name) is no more!")
}
Source: https://www.hackingwithswift.com/sixty/8/6/deinitializers

Keep reference on view/data model after View update

Consider we have a RootView and a DetailView. DetailView has it's own BindableObject, let's call it DetailViewModel and we have scenario:
RootView may be updated by some kind of global event e.g. missed
internet connection or by it's own data/view model
When RootView handling event it's
content is updated and this is causes new struct of DetailView to
be created
If DetailViewModel is created by DetailView on init,
there would be another reference of DetailViewModel and it's state (e.g. selected object) will be missed
How can we avoid this situation?
Store all ViewModels as EnvironmentObjects that is basically a singleton pool. This approach is causes to store unneeded objects in memory when they are not used
Pass throw all ViewModels from RootView to it's children and to children of child (has cons as above + painfull dependencies)
Store View independent DataObjects (aka workers) as EnvironmentObjects. In that case where do we store view dependent states that corresponds to Model? If we store it in View it will end up in situation where we cross-changing #States what is forbidden by SwiftUI
Better approach?
Sorry me for not providing any code. This question is on architecture concept of Swift UI where we trying to combine declarative structs and reference objects with data.
For now I don't see da way to keep references that corresponds to appropriate view only and don't keep them in memory/environment forever in their current states.
Update:
Lets add some code to see whats happening if VM is created by it's View
import SwiftUI
import Combine
let trigger = Timer.publish(every: 2.0, on: .main, in: .default)
struct ContentView: View {
#State var state: Date = Date()
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: ContentDetailView(), label: {
Text("Navigation push")
.padding()
.background(Color.orange)
})
Text("\(state)")
.padding()
.background(Color.green)
ContentDetailView()
}
}
.onAppear {
_ = trigger.connect()
}
.onReceive(trigger) { (date) in
self.state = date
}
}
}
struct ContentDetailView: View {
#ObservedObject var viewModel = ContentDetailViewModel()
#State var once = false
var body: some View {
let vmdesc = "View model uuid:\n\(viewModel.uuid)"
print("State of once: \(once)")
print(vmdesc)
return Text(vmdesc)
.multilineTextAlignment(.center)
.padding()
.background(Color.blue)
.onAppear {
self.once = true
}
}
}
class ContentDetailViewModel: ObservableObject, Identifiable {
let uuid = UUID()
}
Update 2:
It seems that if we store ObservableObject as #State in view (not as ObservedObject) View keeps reference on VM
#State var viewModel = ContentDetailViewModel()
Any negative effects? Can we use it like this?
Update 3:
It seems that if ViewModel kept in View's #State:
and ViewModel is retained by closure with strong reference - deinit will never be executed -> memory leak
and ViewModel is retained by closure with weak reference - deinit invokes every time on view update, all subs will be reseted, but properties will be the same
Mehhh...
Update 4:
This approach also allows you to keep strong references in bindings closures
import Foundation
import Combine
import SwiftUI
/**
static func instanceInView() -> UIViewController {
let vm = ContentViewModel()
let vc = UIHostingController(rootView: ContentView(viewModel: vm))
vm.bind(uiViewController: vc)
return vc
}
*/
public protocol ViewModelProtocol: class {
static func instanceInView() -> UIViewController
var bindings: Set<AnyCancellable> { get set }
func onAppear()
func onDisappear()
}
extension ViewModelProtocol {
func bind(uiViewController: UIViewController) {
uiViewController.publisher(for: \.parent)
.sink(receiveValue: { [weak self] (parent) in
if parent == nil {
self?.bindings.cancel()
}
})
.store(in: &bindings)
}
}
struct ModelView<ViewModel: ViewModelProtocol>: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ModelView>) -> UIViewController {
return ViewModel.instanceInView()
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<ModelView>) {
//
}
}
struct RootView: View {
var body: some View {
ModelView<ParkingViewModel>()
.edgesIgnoringSafeArea(.vertical)
}
}
Apple says that we should use ObservableObject for the data that lives outside of SwiftUI. It means you have to manage your data source yourself.
It looks like a single state container fits best for SwiftUI architecture.
typealias Reducer<State, Action> = (inout State, Action) -> Void
final class Store<State, Action>: ObservableObject {
#Published private(set) var state: State
private let reducer: Reducer<State, Action>
init(initialState: State, reducer: #escaping Reducer<State, Action>) {
self.state = initialState
self.reducer = reducer
}
func send(_ action: Action) {
reducer(&state, action)
}
}
You can pass the instance of the store into the environment of your SwiftUI app and it will be available in all views and will store your app state without data losses.
I wrote a blog post about this approach, take a look at it for more information
https://swiftwithmajid.com/2019/09/18/redux-like-state-container-in-swiftui/

Resources