Weakly captured self won't let the view model deallocate until the Task finishes - swift

I am trying to learn ARC and I'm having a hard time with a weakly captured self. My project is using MVVM with SwiftUI. I'm presenting a sheet (AuthenticationLoginView) that has a #StateObject var viewModel = AuthenticationLoginViewModel() property. On dismiss of the presented sheet, I expect that the viewModel will have it's deinit called and so it does until I run an asynchronous function within a Task block.
class AuthenticationLoginViewModel: ObservableObject {
#Published var isLoggingIn: Bool = false
private var authenticationService: AuthenticationService
private var cancellables: Set<AnyCancellable> = Set()
private var onLoginTask: Task<Void, Never>?
init(authenticationService: AuthenticationService) {
self.authenticationService = authenticationService
}
deinit {
onLoginTask?.cancel()
LoggerService.log("deallocated")
}
public func onLogin() {
guard !isLoggingIn else { return }
isLoggingIn = true
onLoginTask = Task { [weak self] in
await self?.login()
}
}
private func login() async {
LoggerService.log("Logging in...")
sleep(2)
//
// self is still allocated here <<<---- ???
//
let authResponse = try? await self.authenticationService.authenticate(username: username, password: password)
LoggerService.log(self.isLoggingIn) // <<--- prints `true`
handleLoginResponse(authResponse: authResponse)
}
}
So I have my two cases here:
Case #1
I present the sheet.
I dismiss the sheet.
The deinit function is getting called (app logs: "deallocated")
Case #2
I present the sheet.
I press the login button so the onLogin function is getting called.
I dismiss the sheet before the sleep(2) ends.
---- I EXPECT the "deallocated" message to be printed from deinit and the logging at LoggerService.log(self.isLoggingIn) to print nil and the self.authenticationService.authenticate(... to never be called as self doesn't exist anymore.
Not expected but happening: the app prints "Logging in", sleeps for 2 seconds, calls the service, prints true, and then deallocates the view model (the view was dismissed 2 seconds ago)
What am I doing wrong?
I'm still learning and I'm pretty much unsure if this is normal or I miss something. Anyway, I expect the view model to be deallocated as the view referencing it was dismissed.

At the time you call onLogin the reference to self is valid and so the Task commences.
After that, the reference to self in login keeps self alive. The Task has a life of its own, and you did not cancel it.
Moreover the use of sleep is wrong, as it is not cancellable in any case, so neither is your Task. Use Task.sleep.

Related

How to observe login state?

I want View Controllers to be aware of every change in login status. Do I have to make a single tone and subscribe?
Singleton.swift
class Singleton {
static let shared = Singleton()
let isLogin: BehaviorRelay<Bool>
private init() {
isLogin = BehaviorRelay<Bool>(value: false)
}
}
SomeViewController
class SomeVc: UIViewController {
Sigleton.shared.isLogin.subscribe(.....)
}
No you don't need a Singleton...
Here's code I use in actual production. This code is in my application(_:didFinishLaunchingWithOptions:) method.
_ = UserDefaults.standard.rx.observe(String.self, "token")
.map { $0 ?? "" }
.filter { $0.isEmpty }
.bind(onNext: presentScene(animated: true) { _ in
LoginViewController.scene { $0.connect() }
})
When the user logs in, I save a token in UserDefaults, when the user logs out, I remove it. The above code will present my LoginViewController when the user logs out.
If any other view controller needs to track the login state of the user, they can also subscribe to the token observable.
The presentScene(animated:_:) function and scene(_:) method both come from my CLE Library

Swift calling a ViewController function from the AppDelegate [duplicate]

I am building an iOS app using the new language Swift. Now it is an HTML5 app, that displays HTML content using the UIWebView. The app has local notifications, and what i want to do is trigger a specific javascript method in the UIWebView when the app enters foreground by clicking (touching) the local notification.
I have had a look at this question, but it does not seem to solve my problem. I have also come across this question which tells me about using UIApplicationState, which is good as that would help me know the the app enters foreground from a notification. But when the app resumes and how do i invoke a method in the viewController of the view that gets displayed when the app resumes?
What i would like to do is get an instance of my ViewController and set a property in it to true. Something as follows
class FirstViewController: UIViewController,UIWebViewDelegate {
var execute:Bool = false;
#IBOutlet var tasksView: UIWebView!
}
And in my AppDelegate i have the method
func applicationWillEnterForeground(application: UIApplication!) {
let viewController = self.window!.rootViewController;
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var setViewController = mainStoryboard.instantiateViewControllerWithIdentifier("FirstView") as FirstViewController
setViewController.execute = true;
}
so what i would like to do is when the app enters foreground again, i want to look at the execute variable and run the method as follows,
if execute{
tasksView.stringByEvaluatingJavaScriptFromString("document.getElementById('sample').click()");
}
Where should i put the code for the logic to trigger the javascript from the webview? would it be on viewDidLoad method, or one of the webView delegate methods? i have tried to put that code in the viewDidLoad method but the value of the boolean execute is set to its initial value and not the value set in the delegate when the app enters foreground.
If I want a view controller to be notified when the app is brought back to the foreground, I might just register for the UIApplication.willEnterForegroundNotification notification (bypassing the app delegate method entirely):
class ViewController: UIViewController {
private var observer: NSObjectProtocol?
override func viewDidLoad() {
super.viewDidLoad()
observer = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}
}
deinit {
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
Note, in the completion closure, I include [unowned self] to avoid strong reference cycle that prevents the view controller from being deallocated if you happen to reference self inside the block (which you presumably will need to do if you're going to be updating a class variable or do practically anything interesting).
Also note that I remove the observer even though a casual reading of the removeObserver documentation might lead one to conclude is unnecessary:
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.
But, when using this block-based rendition, you really do need to remove the notification center observer. As the documentation for addObserver(forName:object:queue:using:) says:
To unregister observations, you pass the object returned by this method to removeObserver(_:). You must invoke removeObserver(_:) or removeObserver(_:name:object:) before any object specified by addObserver(forName:object:queue:using:) is deallocated.
I like to use the Publisher initializer of NotificationCenter. Using that you can subscribe to any NSNotification using Combine.
import UIKit
import Combine
class MyFunkyViewController: UIViewController {
/// The cancel bag containing all the subscriptions.
private var cancelBag: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
addSubscribers()
}
/// Adds all the subscribers.
private func addSubscribers() {
NotificationCenter
.Publisher(center: .default,
name: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.doSomething()
}
.store(in: &cancelBag)
}
/// Called when entering foreground.
private func doSomething() {
print("Hello foreground!")
}
}
Add Below Code in ViewController
override func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector:#selector(appMovedToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appMovedToForeground() {
print("App moved to foreground!")
}
In Swift 3, it replaces and generates the following.
override func viewDidLoad() {
super.viewDidLoad()
foregroundNotification = NotificationCenter.default.addObserver(forName:
NSNotification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) {
[unowned self] notification in
// do whatever you want when the app is brought back to the foreground
}

SwiftUI ObservedObject not updating view after successful login

To show my the content from my root view after successful login, I tried with ObservedObject, and with EnvironmentObject, to no avail.
E.g. as follows:
struct RootView: View {
#EnvironmentObject var loginManager: LoginManager
var body: some View {
Group {
if loginManager.isLoggedIn {
SegmentedView()
}
else {
WelcomeView()
}
}
}
}
class LoginManager: ObservableObject {
static let shared = LoginManager()
var cancellable = Set<AnyCancellable>()
#Published var isLoggedIn = false
...
func login(...) {
...
// on success
self.isLoggedIn = true
}
The LoginManager is retained in the SceneDelegate and put into the environment:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var loginManager = LoginManager.shared
...
// the view passed to window.rootUiewController via UIHostingController
let contentView = RootView().environmentObject(loginManager)
After logging in, it goes right back to my WelcomeView. What am I missing?
EDIT
Here is a new aspect. I have a view model for the LoginView to manage the data in date fields. When the login button is pressed, I call a login() method in this view model.
I need to two .sink callbacks in the view model, because I have to dismiss the loading indicator by setting a loading flag to false.
So I cannot call self.isLoggedIn = true directly because I am in the view model, not the LoginManager. Instead I call
self.loginManager.isLoggedIn = true
and I suspect that this line is not working.
The connection between view model and login manager is done like this
#ObservedObject var loginManager = LoginManager.shared
However, after shifting this to the LoginManager, I am indeed calling self.isLoggedIn from there. It is still not working.
I have two theses:
It could be that the view is not set up correctly with Group etc. I also tried to use #ViewBuilder etc, no difference.
It could be that somehow there are two instances of LoginManager, or the RootView somehow get's reinitialised with a new instance where isLoggedIn is false. But I have been creating Swift singletons like this for ages:
static let shared = LoginManager()
and never had any problems.
As mentioned in the comments, there is another error I encountered when switching all to #EnvironmentObject:
Fatal error: No ObservableObject of type LoginManager found. A View.environmentObject(_:) for LoginManager may be missing as an ancestor of this view.: file SwiftUI, line 0
I think I got the error.
I assumed .sink(receiveCompletion: always has an error because the template to which I was referring to it. But the completion block is called no matter what.
Unfortunately, if you insert a parameter error in it will never be nil but contain something like
Combine.Subscribers.Completion<module.CustomError>.finished
So I guess, you are not supposed to check for errors there again.

Why does this combine subscription not deallocate in custom ViewModifier?

In the documentation for assign it says the following...
The Subscribers/Assign instance created by this operator maintains a
strong reference to object, and sets it to nil when the upstream
publisher completes (either normally or with an error).
In the ViewModifier below the assign method in subscribeToKeyboardChanges() refers to self but self is a struct here so there's no way it can create a strong reference
Why doesn't the subscription in subscribeToKeyboardChanges() get immediately deallocated?
What is the actually happening here behind the scenes?
struct KeyboardHandler: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, self.keyboardHeight)
.animation(.default)
.onAppear(perform: subscribeToKeyboardChanges)
}
private let keyboardWillShow = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
.map { $0.height }
private let keyboardWillHide = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat.zero }
private func subscribeToKeyboardChanges() {
_ = Publishers.Merge(keyboardWillShow, keyboardWillHide)
.subscribe(on: DispatchQueue.main)
.assign(to: \.self.keyboardHeight, on: self)
}
}
I believe you`re referring to the wrong function description. Here is right one:
assign(to:on:)
Assigns a publisher’s output to a property of an object.
...
Return Value
An AnyCancellable instance. Call cancel() on this instance when you no
longer want the publisher to automatically assign the property.
Deinitializing this instance will also cancel automatic assignment.
So in your subscribeToKeyboardChanges example code it's expected that subscription will be canceled after function finishes. You have to keep strong reference to AnyCancellable return from assign to keep subscription in memory.
EDIT:
It appears that in this line assign copies self and holds it in the memory until cancel() call.
.assign(to: \.self.keyboardHeight, on: self)
Therefore View that uses KeyboardHandler view modifier will never be deallocated with subscription and will eventually bloat the memory during navigation. For example, here is the screenshot after 3 navigations see 3 instances of KeyboardHandler still in the memory.

MVP - Destroy Presenter object from View Controller on dismiss in iOS

I'm following Example Here to apply MVP pattern in swift.
When I dismiss my View Controller, the presenter is not destroyed and View Controller also remains in memory.
When I try to make the presenter object 'weak', the code break at this line.
presenter.login(email: "email", password: "password")
How can I properly declare and destroy the presenter instance. Thanks
In your code in Presenter you create request to network and capture self in closure without using weak or unowned reference. Because of this there is a retain cycle. You can read more about retain cycles here.
Updated code:
func login(email: String, password: String)
{
self.view.showProgress()
FoodAPI.api.login(email: email, password: password) { [weak self] (msg, user) in
guard let `self` = self else {
return
}
DispatchQueue.main.async {
self.view.hideProgress()
if let user = user
{
AppDelegate.shared.user = user
UserDefaultsHelper.saveUser(user: user)
self.view.openMenu() //this line will dismiss the VC and presents next one.
}else
{
self.view.showAlert(message: msg)
}
}
}
}
If you want to see advanced usage of MVP pattern you can checkout my open project here and feel free to ask me anything.