SwiftUI and ObservableObject ViewModels causes memory leaks, any example how should I use Combine in ObservableObject - memory-leaks

I have such example ViewModel and what I can see in Instruments is that such and similar ViewModel is leaking memory
class SearchViewModel<T>: ObservableObject {
#Published var searchTerm : String = ""
#Published var results: [T] = []
private var disposables = Set<AnyCancellable>()
init() {
_searchTerm.projectedValue
//$searchTerm
.debounce(for: .milliseconds(350), scheduler: DispatchQueue.global())
.flatMap { [weak self] term -> AnyPublisher<[T], Never> in
self?.search(by: term) ?? Empty().eraseToAnyPublisher()
}
.print("searching")
.receive(on: DispatchQueue.main)
.assignNotRetain(to: \.results, on: self)
//.assign(to: \.results, on: self)
.store(in: &disposables)
}
open func search(by term: String) -> AnyPublisher<[T], Never> {
fatalError()
}
}
I've added [weak self] in flatMap and .assign(to) changed to custom .assignNotRetain(to) which uses sink with [weak self] it leaks less (there are deinit calls) but there is more init calls then deinit calls! This causes disposables: Set to be not deallocated/cancelled subscriptions
It just example view model similar approach with calling serivce and then observing output via sink, assign and updating #Published all happens to leak memory and disposables usually are not deallocated so, should I cancell AnyCancellable manually in View onDisappear?

Related

Problem using #Environment variable in SwiftUI constructor

I'm using a core data child context to separate temporary entities from my main context, but when I pass in the main context as an environment variable, I don't have access to it in order to initialize the child context and get a 'self' used before all stored properties are initialized error
Any suggestions?
struct CreatePlayerView: View {
#State private var newPlayer: PlayerEntity
#Environment(\.managedObjectContext) private var managedObjectContext
private let childManagedObjectContext: NSManagedObjectContext
init() {
childManagedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
\\ 'self' used before all stored properties are initialized
childManagedObjectContext.parent = managedObjectContext
newPlayer = PlayerEntity(context: childManagedObjectContext)
}
var body: some View {
EditPlayerViewDetail(
player: $newPlayer,
onDone: { player in
try? childManagedObjectContext.save()
}
)
}
}

Converting Key-Value Observation in AppKit/UIKit to Combine and SwiftUI

I'm having trouble wrapping my head around how to use Combine in SwiftUI. I'm accustomed to using key-value observation in AppKit an UIKit because view controllers don't need to know about each other and can just react to some global objects that help determine state.
For example, in an AppKit/UIKit app, I would create a global state object like this:
//Global State file
#objc class AppState: NSObject {
#objc dynamic var project: Project?
}
//Create an instance I can access anywhere in my app
let app = AppState()
Then in a view controller, I can get notified of any changes to my app-wide project instance and react accordingly:
//View Controller
class MyViewController: NSViewController{
var observerProject: NSKeyValueObservation?
override func viewDidLoad() {
observerProject = app.observe(\.project) { object, change in
self.refreshData()
}
}
func refreshData(){
//Query my persistent store and update my UI
}
}
What is the Combine/SwiftUI analog to this?
Do I need to create a Publisher and then listen to my global object changes? If so, how do I make my Core Data #FetchRequest (whose predicate includes my global Project object) respond in real-time?
I've done things the old way for so long that this transition to SwiftUI/Combine is rather confusing to me. 🙂
#FetchRequest doesn't work well with a dynamic predicate (There are some workarounds in SO) you will have to use the "old school" NSFetchedResultsController for that and put it into an ObservableObject. Here is a video with a sample. It is a lot of setup and code.
import SwiftUI
import Combine
class AppState: ObservableObject {
static let shared = AppState()
#Published var project: String?
//Just to mimic updates
var count = 0
private init() {
//Mimic getting updates to project
Timer.scheduledTimer(withTimeInterval: 2, repeats: true){ timer in
print("Timer")
self.project = self.count.description
self.count += 1
if self.count >= 15{
timer.invalidate()
}
}
}
}
class MyViewModel: ObservableObject{
#Published var refreshedData: String = "init"
let appState: AppState = AppState.shared
var projectCancellable: AnyCancellable?
init() {
projectCancellable = appState.$project.sink(receiveValue: {
value in
print(value ?? "nil")
self.refreshData()
})
}
func refreshData() {
refreshedData = Int.random(in: 0...100).description
}
}
struct MyView: View {
#StateObject var vm: MyViewModel = MyViewModel()
var body: some View {
VStack{
Text(vm.refreshedData)
}
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView()
}
}

Can I notify a BehaviorProcessor from inside a RxJava stream?

I would like to get your feedback on the code below.
I'm wondering if it's safe to call currentSession.onNext(result.session)
from inside the SessionManager.signIn stream.
My first intuition is to say NO because of multithreading and synchronization issues, meaning, based on this code, I could be calling currentSession.onNext(result.session) from different threads.
Here is the code, please let me know what you think! Thanks
SessionManager which is a singleton
#Singleton
class SessionManager #Inject constructor(
private val sessionService: SessionService,
){
val currentSession = BehaviorProcessor.create<Session>()
fun signIn(login: String, password: String): Single<Boolean> =
sessionService.signIn(login, password)
.doOnNext(result ->
if (session is Success) {
currentSession.onNext(result.session)
}
).map { result ->
when (result) {
is Success -> true
else -> false
}
}
.subscribeOn(Schedulers.io())
}
HomeView which is a random View subscribing to the SessionManager's signIn stream
class HomeView(val context: Context) : View(context) {
#Inject
lateinit var sessionManager: SessionManager
private val disposables = CompositeDisposable()
override fun onAttachedToWindow() {
super.onAttachedToWindow()
disposables.add(sessionManager.signIn("username", "password")
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
textView.text = if (result) "Success" else "Fail"
})
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
disposables.clear()
}
}
A random View observing the currentSession from SessionManager
class RandomView(val context: Context) : View(context) {
#Inject
lateinit var sessionManager: SessionManager
private val disposables = CompositeDisposable()
override fun onAttachedToWindow() {
super.onAttachedToWindow()
disposables.add(sessionManager.currentSession
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { session -> userTextView.text = session.userName })
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
disposables.clear()
}
}
The documentation of BehaviorProcessor says:
Calling onNext(Object), offer(Object), onError(Throwable) and onComplete() is required to be serialized (called from the same thread or called non-overlappingly from different threads through external means of serialization). The FlowableProcessor.toSerialized() method available to all FlowableProcessors provides such serialization and also protects against reentrance (i.e., when a downstream Subscriber consuming this processor also wants to call onNext(Object) on this processor recursively).
So if you define it like this:
val currentSession = BehaviorProcessor.create<Session>().toSerialized()
then you can safely call onNext from any thread, it will not cause any synchronisation problems.
Notes:
I agree that the update of the processor should be in a doOnNext instead of the map.
I think it would be better to use a Completable instead of a Single<Boolean>, and use Rx errors to indicate what prevented signing in. You should also define the error handlers in the subscribe methods.

Swift passing structs between view controllers?

My app has the user log into their account. Then, the app accesses Parse.com to retrieve all of their information. That information is stored in a struct. I want to have that struct passed between view controllers so it can be accessed at any time in the app. Everything I've tried gives me errors. I can't declare it as an optional, or set up an identical struct in the next class. What is the solution to this?
Use the following code to pass struct between ViewControllers
//FirstViewController.swift
struct GlobalStruct
{
var details:String;
init()
{
details = "global struct";
}
};
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func buttonClicked()
{
let secondViewController = self.storyboard.instantiateViewControllerWithIdentifier("secondview") as SecondViewController
var passData = GlobalStruct();
passData.details = "Secret Information :)";
secondViewController.setStructDataReference(passData); //passStruct
self.navigationController.pushViewController(secondViewController, animated: true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
//SecondViewController.swift
class SecondViewController:UIViewController
{
var structData:GlobalStruct;
override func viewDidLoad()
{
println("struct data = \(self.structData.details)");
}
func setStructDataReference(structDataReference:GlobalStruct)
{
self.structData = structDataReference;
}
init(coder aDecoder: NSCoder!)
{
self.structData = GlobalStruct();
super.init(coder: aDecoder);
}
}
I would create empty arrays [Struct] on both controllers.
Then in whatever action you perform before performSegue you should do:
self.arrayinmaincontroller.insert(the struct, at:0)
and pass it in func prepare(for segue: UIStoryboardSegue, sender: Any?)

Using Swift protocols with generics

I have a simple example that seems like it should work:
import CoreData
#objc protocol CoreDataModel {
#optional class func entityName() -> String
}
class AbstractModel: NSManagedObject, CoreDataModel {
class func create<T : CoreDataModel>(context:NSManagedObjectContext) -> T {
var name = T.entityName?()
var object = NSEntityDescription.insertNewObjectForEntityForName(name, inManagedObjectContext: context) as T
return object
}
}
So we have a class called AbstractModel which conforms to the protocol CoreDataModel, and CoreDataModel defines an optional class method called entityName.
However, this line:
var name = T.entityName?()
causes the error:
Expected member name or constructor call after type name
Any idea what I'm doing wrong?
Edit
Removing the word #optional from the declaration and changing the function a bit allows the code to compile, but now I get a runtime error saying that the
'Swift dynamic cast failed'
#objc protocol CoreDataModel {
class func entityName() -> String
}
class AbstractModel: NSManagedObject, CoreDataModel {
class func entityName() -> String {
return "AbstractModel"
}
class func create<T : CoreDataModel>(aClass:T.Type, context:NSManagedObjectContext) -> T {
var name = aClass.entityName()
var object = NSEntityDescription.insertNewObjectForEntityForName(name, inManagedObjectContext: context) as T
return object
}
}
I cannot explain why your code causes a runtime exception. But it works if you change
the function prototype
class func create<T : CoreDataModel>(aClass:T.Type, context:NSManagedObjectContext) -> T
to
class func create<T : NSManagedObject where T: CoreDataModel>(aClass:T.Type, context:NSManagedObjectContext) -> T
Assuming that your managed object subclass conforms to the protocol, for example
extension Event : CoreDataModel {
class func entityName() -> String {
return "Event"
}
}
then this works and creates a new object:
let newManagedObject = AbstractModel.create(Event.self, context: context)
Alternatively, you could use the approach from the answer to
"Swift: return Array of type self" and
define an extension to the NSManagedObjectContext class:
extension NSManagedObjectContext {
func create<T : NSManagedObject where T : CoreDataModel >(entity: T.Type) -> T {
var classname = entity.entityName()
var object = NSEntityDescription.insertNewObjectForEntityForName(classname, inManagedObjectContext: self) as T
return object
}
}
Then a new object would be created as
let newManagedObject = context.create(Event.self)
From "The Swift Programming Language"
Because T is a placeholder, Swift does not look for an actual type called T.
As T is not a real type, it is maybe not useful to cast to T.

Resources