ObservedObject ViewModel Memory Leak with SwiftUI - ios

I am experiencing memory leaks and i'm not sure the correct way to go about it while preserving the ViewModel approach.
So let's pretend we have a note-taking/scribbling app app in which you can present a screen and create a new "scribble".
Our ViewModel looks something like this...
class ScribbleViewModel: ViewModel<ScribbleContext>, ObservableObject {
#Published var shouldShowSaveButton: Bool = false
#Published var text: String {
didSet {
// Ignore duplicates
guard oldValue != text else { return }
shouldShowSaveButton = isTextValidForSaving(text)
}
}
// down here we just have logic for user tracking dismissing/saving, etc
}
and our SwiftUI view looks something like this:
struct ScribbleView: View {
#ObservedObject var viewModel: ScribbleViewModel
init(viewModel: ScribbleViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
ScribblesViewHeader(
shouldShowSaveButton: viewModel.shouldShowSaveButton,
closeAction: { [weak viewModel] in
viewModel?.userDidPressDismissButton()
},
saveAction: { [weak viewModel] in
viewModel?.userDidPressSaveButton()
}
)
ScribbleInputView(text: $viewModel.text)
.padding(Constants.horizontalEdgeInsets)
}.navigationBarHidden(true).onAppear { [weak viewModel] in
viewModel?.userDidLandOnScreen()
}
}
}
The view is created and presented as such (as you can see, we are not holding a reference to it:
let requiredContext = /* context required for the module */
let viewModel = ScribbleViewModel(context: requiredContext)
let scribbleView = ScribbleView(viewModel: viewModel)
let hostingController = UIHostingController(rootView: scribbleView)
let navigationController = UINavigationController(rootViewController: hostingController)
navigationController.modalPresentationStyle = .fullScreen
present(navigationController, animated: true, completion: nil)
When I dismiss the view, the ViewModel remains in memory, seemingly being held by its HostingController or its bindings. Its presenting view has no held reference to it, so i'm confused as to why this is happening. Am i not supposed to be binding my SwiftUI views to a held class?
I've made the references to the viewController in the childview actions weak, but do i also have to do something with the bindings?

Related

ObservedObject is still in memory after the view is dismissed, Memory Leak?

I'm making an app with SwiftUI and UIkit, I use UIkit for the main app controller and navigation, and I use SwiftUI for app design.
The app works very well, but I'm worried about the memory leaks. This is because the ViewModels I use to pass data between views don't call desinit whene the view disappears. I know that in SwiftUI views are not disposed immediately, but since I'm using UIKit to navigate I don't know what the problem is.
//The ViewModel for each user fetched
internal class UserViewModel: ObservableObject, Identifiable {
//MARK: - Propeties var currentListener: ListenerRegistration?
#Published var request: Request?
#Published var user: User
init(user: User) {
self.user = user
getRequest()
fetchAdmins()
}
deinit {
//Dosnt get called removeListener()
}
func getRequest() {
guard let uid = Auth.auth().currentUser?.uid else {return}
guard let id = id else {return}
self.currentListener = Collections.requests(id).document(uid).addSnapshotListener { snapshot, error in
if let error = error {
print(error.localizedDescription)
return
}
if ((snapshot?.exists) != nil) {
if let request = try? snapshot!.data(as: Request.self) {
DispatchQueue.main.async {
self.request = request
}
}
}
}
}
func removeListener() {
self.currentListener?.remove()
}
}
}
//The ViewModel to fetch all the users ViewModels
class UsersViewModel: ObservableObject {
#Published var users = [UserViewModel]()
func fetch() {
DispatchQueue.global(qos: .background).async {
Collections.users.getDocuments(completion: { snapshot, err in
guard let documents = snapshot?.documents else { return } let users = documents.compactMap({ try? $0.data(as: User.self) })
users.forEach { user in
let vm = UserViewModel(user: user)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.users.append(vm)
}
}
})
}
} }
//Show the users cells with the ViewModel
struct HomeView: View {
#ObservedObject var usersViewModels: UsersViewModel
//MARK: - Init
init() {
self.usersViewModels = UsersViewModel()
}
var body: some View {
ListView(content: {
ForEach(usersViewModels) { usersViewModel in
UserCell(viewModel: usersViewModel).id(user.id)
}
})
}
}
This is how I navigate between controllers and views of my app. I don't use NavigationLinks:
public static func push<Content: View>(view: Content) {
DispatchQueue.main.async {
guard let tabBarController = UIApplication.rootViewController as? UITabBarController, let navigationController = tabBarController.selectedViewController as? UINavigationController else { return nil }
if let navigationController = UIApplication.getCurrentNavigationController() {
navigationController.pushViewController(HostingController(content: view), animated: true)
}
}
}
Does anyone know if this method that I am using to navigate can cause me memory problems? And you know why my app doesn't reduce its memory every time I close a window, it just increases more and more.
The disappearing does not mean it is no longer in memory.
It looks like you keep pushing them onto the navigation stack which increases their retain count.
You've got a memory leak here:
struct HomeView: View {
#ObservedObject var usersViewModels: UsersViewModel
//MARK: - Init
init() {
self.usersViewModels = UsersViewModel() // here
}
View structs must not init objects because the View struct is recreated every state change thus the object is being constantly init.
SwiftUI is all about taking advantage of value semantics, try to use #State with value types (or group them in a struct) in the View struct for your view data.
Model data structs go in a singleton ObservableObject supplied to the Views using .environmentObject.

SwiftUI, How to dismiss current presenting View when API call returns successfully

I am using SwiftUI and I am trying to achieve a simple logical action but unable to understand SwiftUI action hierarchy.
I have one API call something like this,
final class TaskData: ObservableObject {
#Published var updatedFields = false
#Published var updateMsg = ""
func updateFields()
{
//Some API Call
.whenSuccess { (response) in
DispatchQueue.main.async {
self.updatedFields = true
self.updateMsg = "Successfully updated fields"
//Send Request to dismiss current View ???
}
}
}
}
Now, I have a View something like this, and on a request I want to dismiss this View, but I am unable to find any method for that,
struct TaskView: View {
#Environment(\.presentationMode) var currentView: Binding<PresentationMode>
#EnvironmentObject var taskData: TaskData
var body : some View {
//Some Views here ////
//Need Some code here to dismiss currentView?????
.navigationBarItems(trailing: Button(action: {
}, label: {
Text("Done")
}).onTapGesture {
self.taskData.updateFields() // Method Call to Update fields
})
}
if someone can explain this thing in a little detail as I am newbie to SwiftUI, I have seen a lot tutorial but unable to understand this structure of swift.
It is not shown how TaskView is presented, but having presentationMode in give code snapshot let's assume that it is valid, so the approach might be as follows
#Environment(\.presentationMode) var presentationMode //better to name it same,
//type is extracted from Environment
#EnvironmentObject var taskData: TaskData
var body : some View {
//Some Views here ////
SomeView()
.onReceive(taskData.$updatedFields) { success in
if success {
self.presentationMode.wrappedValue.dismiss() // dismiss self
}
}
...

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

SwiftUI and UICloudSharingController hate each other

I have a project using SwiftUI that requires CloudKit sharing, but I'm unable to get the UICloudSharingController to play nice in a SwiftUI environment.
First Problem
A straight-forward wrap of UICloudSharingController using UIViewControllerRepresentable yields an endless spinner (see this). As has been done for other system controllers like UIActivityViewController, I wrapped the UICloudSharingController in a containing UIViewController like this:
struct CloudSharingController: UIViewControllerRepresentable {
#EnvironmentObject var store: CloudStore
#Binding var isShowing: Bool
func makeUIViewController(context: Context) -> CloudControllerHost {
let host = CloudControllerHost()
host.rootRecord = store.noteRecord
host.container = store.container
return host
}
func updateUIViewController(_ host: CloudControllerHost, context: Context) {
if isShowing, host.isPresented == false {
host.share()
}
}
}
final class CloudControllerHost: UIViewController {
var rootRecord: CKRecord? = nil
var container: CKContainer = .default()
var isPresented = false
func share() {
let sharingController = shareController
isPresented = true
present(sharingController, animated: true, completion: nil)
}
lazy var shareController: UICloudSharingController = {
let controller = UICloudSharingController { [weak self] controller, completion in
guard let self = self else { return completion(nil, nil, CloudError.controllerInvalidated) }
guard let record = self.rootRecord else { return completion(nil, nil, CloudError.missingNoteRecord) }
let share = CKShare(rootRecord: record)
let operation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: [])
operation.modifyRecordsCompletionBlock = { saved, _, error in
if let error = error {
return completion(nil, nil, error)
}
completion(share, self.container, nil)
}
self.container.privateCloudDatabase.add(operation)
}
controller.delegate = self
controller.popoverPresentationController?.sourceView = self.view
return controller
}()
}
This allows the controller to come up normally, but...
Second Problem
Tap the close button or swipe to dismiss and the controller will disappear, but there's no notification that it's been dismissed. The SwiftUI view's #State property that initiated presenting the controller is still true. There's no obvious method to detect dismissal of the modal. After some experimenting, I discovered the presenting controller is the original UIHostingController created in the SceneDelegate. With some hackery, you can inject an object that is referenced in a UIHostingController subclass into the CloudSharingController. This will let you detect the dismissal and set the #State property to false. However, all nav bar buttons no longer function after dismissing so you could only ever tap this thing once. The rest of the scene is completely functional, but buttons in the nav bar don't respond.
Third Problem
Even if you could get the UICloudSharingController to present and dismiss normally, tapping on any of the sharing methods (Messages, Mail, etc) makes the controller disappear with no animation and the controller for the sharing URL doesn't come up. No crash or console messages--it just disappears.
Demo
I made a quick and dirty project on GitHub to demonstrate the issue: CloudKitSharing. It just creates a single String and a CKRecord to represent it using CloudKit. The interface displays the String (a UUID) with a single nav bar button to share it:
The Plea
Is there any way to use UICloudSharingController in SwiftUI? Don't have the time to rebuild the project in UIKit or a custom sharing controller (I know--the price of being on the bleeding edge 💩)
I got this working -- initially, I wrapped the UICloudSharingController in a UIViewControllerRepresentable, much like the link you provided (I referenced that while building it), and simply adding it to a SwiftUI .sheet() view. This worked on the iPhone, but it failed on the iPad, because it requires you to set the popoverPresentationController?.sourceView, and I didn't have one, given that I triggered the sheet with a SwiftUI Button.
Going back to the drawing board, I rebuilt the button itself as a UIViewRepresentable, and was able to present the view using the rootViewController trick that SeungUn Ham suggested here. All works, on both iPhone and iPad - at least in the simulator.
My button:
struct UIKitCloudKitSharingButton: UIViewRepresentable {
typealias UIViewType = UIButton
#ObservedObject
var toShare: ObjectToShare
#State
var share: CKShare?
func makeUIView(context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) -> UIButton {
let button = UIButton()
button.setImage(UIImage(systemName: "person.crop.circle.badge.plus"), for: .normal)
button.addTarget(context.coordinator, action: #selector(context.coordinator.pressed(_:)), for: .touchUpInside)
context.coordinator.button = button
return button
}
func updateUIView(_ uiView: UIButton, context: UIViewRepresentableContext<UIKitCloudKitSharingButton>) {
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICloudSharingControllerDelegate {
var button: UIButton?
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
//Handle some errors here.
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return parent.toShare.name
}
var parent: UIKitCloudKitSharingButton
init(_ parent: UIKitCloudKitSharingButton) {
self.parent = parent
}
#objc func pressed(_ sender: UIButton) {
//Pre-Create the CKShare record here, and assign to parent.share...
let sharingController = UICloudSharingController(share: share, container: myContainer)
sharingController.delegate = self
sharingController.availablePermissions = [.allowReadWrite]
if let button = self.button {
sharingController.popoverPresentationController?.sourceView = button
}
UIApplication.shared.windows.first?.rootViewController?.present(sharingController, animated: true)
}
}
}
Maybe just use rootViewController.
let window = UIApplication.shared.windows.filter { type(of: $0) == UIWindow.self }.first
window?.rootViewController?.present(sharingController, animated: true)

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