How to test deinit of viewController - ios

I want to test if I remove all the key value observers in the deinit of my view controller.
In the test class I have defined following method to start view controller lifecycle
func startLifecycle() {
_ = viewController.view
}
In my test method I'm trying to invoke deinit by simply assigning nil to my view controller instance
testViewController = nil
XCTAssert for stuff...
But deinit is not called when I execute my test. I see no obvious retain cycles in my VC's code, what's more when I run the code in my app, not the test environment, deinit is called so it doesn't seem like something is keeping view controller in memory.
What is the correct way to release a view controller when testing?

I had the same problem.
When you examine the memory graph, you will see that an object of type UIStoryboardScene maintains a reference to your UIViewController via an #autorelease property 'sceneViewController'.
If you're unfamiliar with #autorelease, this is a helpful article: https://swiftrocks.com/autoreleasepool-in-swift. It explains that objects created with autorelease are released at the end of the current thread's run loop, but not before.
In Swift, we can use autoreleasepool to release UIStoryboardScene's reference to the UIViewController.
It might look something like this:
var testViewController: UIViewController?
autoreleasepool {
testViewController = UIStoryboard(name: "main", bundle: nil).instantiateInitialViewController()
}
Then, when you execute testViewController = nil, the UIViewController will actually deinit.

Try smth like this
class ViewController: UIViewController {
var deinitCalled: (() -> Void)?
deinit { deinitCalled?() }
}
class ViewControllerTest: XCTestCase {
func test_deinit() {
var instance: ViewController? = ViewController()
let exp = expectation(description: "Viewcontroller has deinited")
instance?.deinitCalled = {
exp.fulfill()
}
DispatchQueue.global(qos: .background).async {
instance = nil
}
waitForExpectations(timeout: 2)
}
}

In case someone else needs this i managed to do it this way.
// Given
var optionalSubject: UIViewController? = UIViewController()
// When
optionalSubject = nil // Call deinit
// Then
// TEST DEINIT

Related

present a ViewController from a Swift class derived from an NSObject?

This project was written in Objective C and a bridging header and Swift files were added so they can be used at run time. When the app starts, Initializer called in Mock API client is printed in the debugger. Is it possible to present a ViewController from the initializer?
Xcode Error:
Value of type 'MockApiClient' has no member 'present'
//MockApiclient.Swift
import Foundation
class MockApiClient: NSObject
{
override init ()
{
print("Initializer called in Mock API client")
if isLevelOneCompleted == false
{
print("It's false")
let yourVC = ViewController()
self.present(yourVC, animated: true, completion: nil)
} else
{
print("It's true")
}
}
var isLevelOneCompleted = false
#objc func executeRequest()
{
print("The execute request has been called")
isLevelOneCompleted = true
if isLevelOneCompleted {
print("It's true")
} else {
//do this
}
}
}
Update - ViewController.m
// prints "The execute request has been called" from the debugger window
- (void)viewDidLoad {
[super viewDidLoad];
MockApiClient *client = [MockApiClient new];
[client executeRequest];
}
You can't call present(_:animated:completion) because it is a method of UIViewController, not NSObject.
Why not pass a viewController reference to the MockApiClient to present on instead like so. Be sure to check Leaks or Allocations on instruments to avoid the client retaining the controller.
class MockApiClient: NSObject {
var referencedViewController: UIViewController?
override init() {
let presentableViewController = ViewController()
referencedViewController.present(presentableViewController, animated: true, completion: nil)
}
deinit {
referencedViewController = nil
}
}
let apiClient = MockApiClient()
apiClient.referencedViewController = // The view controller you want to present on
Assuming you're using UIKit, you'll have to present the view controller from the nearest available attached view controller. If you know for certain that no other view controllers would currently be presented then you can safely present from the root view controller:
UIApplication.shared.keyWindow?.rootViewController?.present(someViewController, animated: true, completion: nil)
This concept of attached and unattached/detached view controllers is never officially explained but the infamous UIKit warning of presenting view controllers on detached view controllers is real. And the workaround is finding the nearest available attached view controller, which at first (when nothing is currently being presented) is the root view controller (of the window). To then present an additional view controller (while one is currently being presented), you'd have to present from that presented view controller or its nearest parent view controller if it has children (i.e. if you presented a navigation view controller).
If you subclass UIViewController, you can add this functionality into it to make life easier:
class CustomViewController: UIViewController {
var nearestAvailablePresenter: UIViewController {
if appDelegate.rootViewController.presentedViewController == nil {
return appDelegate.rootViewController
} else if let parent = parent {
return parent
} else {
return self
}
}
}
Then when you wish to present, you can simply do it through this computed property:
nearestAvailablePresenter.present(someViewController, animated: true, completion: nil)

Using CoreData and Dependency Injection - Thread 1: Fatal error: init(coder:) has not been implemented

I'm trying to learn how to use CoreData and the correct way to implement it, currently I have watched this video on youtube (link below). At the moment it all makes sense, however when I go from one viewController to my HomeVC (which is part of a tab bar controller) I get the below error. Does anyone have any idea as to why? Any help is greatly appreciated, many thanks!!
https://www.youtube.com/watch?v=OYRo3i9z-lM
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
\\ Thread 1: Fatal error: init(coder:) has not been implemented
}
Function to home:
func toHome() {
self.dismiss(animated: true) {
if let destVC = UIStoryboard(name: "Home", bundle: nil).instantiateViewController(withIdentifier: "TabBarHomeVC") as? UITabBarController {
if let homeVC = destVC.viewControllers?.first as? HomeVC {
homeVC.persistenceManager = PersistenceManager.shared
self.present(destVC, animated: true, completion: nil)
}
}
}
}
HomeVC:
class HomeVC: UIViewController {
var persistenceManager: PersistenceManager
init(persistenceManager: PersistenceManager) {
self.persistenceManager = persistenceManager
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
PersistenceManager:
import Foundation
import CoreData
final class PersistenceManager {
private init() {}
static let shared = PersistenceManager()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataApp")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
lazy var context = persistentContainer.viewContext
}
If you work with segues then the UIViewController is instantiated from the Storyboard, which is done through the init?(coder:) initializer. You making it unusable makes the HomeVC unable to instantiate itself from Storyboard since it crashes there.
The property var persistenceManager: PersistenceManager makes Swift unable to inherit init(coder:) from UIViewController (explanation) and so you have to provide one for yourself where you initialize all the variables you added, all to get the UIViewController into a usable state.
Try to make var peristenceManager: PersistenceManager optional since you assign it later anyway or assign it a default value in the same line or assign it a value during init?(coder:) so it is initalized. Also call super.init(coder:) inside your init?(coder:) since there it loads all the settings from the Storyboard.
With Storyboard you can’t give things in any initializer so you have to set it after the initializer did run. You can use a static factory function where you initialize the vc instance, then immediately set what you want and then return the vc in a usable state.
So how can I make homeVC.persistenceManager = PersistenceManager.shared, into HomeVC(persistenceManager: PersistenceManager.shared) by passing it through the tabBarController straight to HomeVC?
You can’t use that initializer since the only one being called is the init?(coder:) initializer. You can change the variable to:
var persistenceManager: PersistenceManager!
Then you can set the variable after init(coder:) was called and you have your instance.
Here the UITabBarController initializes the HomeVC, this guy here had the same issue, where to initialize his UIViewController being embedded, maybe it helps you out. The answer uses a call that is being called before a UIViewController is being shown:
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// Initialization code here.
// Also make sure its only called once if you want
}
He even constructs the UIViewController himself if a certain tab is asked for.
You can ofcourse set it as you do already, since the UITabBarController is not shown yet. Keep it simple as you do already.

About strong reference cycles for closures in Swift

I have defined a class called Person. This is my code:
class Person {
var closure: (() -> ())?
var name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
then I use Person in class called ViewController:
class ViewController: UIViewController {
var person = Person(name: "john")
let aStr = "john is a cute boy"
override func viewDidLoad() {
super.viewDidLoad()
person.closure = {
print("\(self.aStr)")
}
person.closure!()
}
}
In my opinion, the picture of memory about my code like this :
So, from above picture, in my opinion, it will cause strong reference cycle between the three instances, but I can not get any leak from Instruments, so I have some confusion.
Does this code cause strong reference cycle?
If not, when will ARC deallocate the instance of Person? the method named deinit in Person class is never called.
Yes, this's a typical retain cycle.
To solve this problem use [weak self] in your closure
person.closure = { [weak self] in
guard let strongSelf = self else { return }
print("\(strongSelf.aStr)")
}
To really create a leak.
I create a demo App. Root is a navController.
The navController has a root controller. Let's call it buttonController.
When you click button in the buttonController, it create your ViewController and push to navController.
When you click back button in navigation bar, the navController pop your ViewController instance.
Profile it, then you will see the leak and the retain cycle in Instruments.
Xcode default template of iOS App use a single page, which always retain your ViewController instance. If the ViewController instance is still used by the system, it's actually not a leak yet.
So push & pop show that leak for you.

GCD keeps strong reference to 'self' even when defining a capture list

class MyClass {
var someProperty = 0
deinit {
// never gets called
}
func doSomething() {
DispatchQueue.global().async { [weak self] in
Thread.sleep(forTimeInterval: 3600)
self?.someProperty = 123
}
}
}
class MyViewController: UIViewController {
var myClass: MyClass?
deinit {
// MyViewController gets properly deallocated
}
override func viewDidLoad() {
super.viewDidLoad()
myClass = MyClass()
myClass?.doSomething()
}
}
When running the above code, MyClass never gets deallocated even when MyViewController is popped of the navigation stack or gets dismissed.
However, if I move the implementation of MyClass directly to MyViewController, everything works as expected:
class MyViewController: UIViewController {
var someProperty = 0
deinit {
// MyViewController still gets properly deallocated
}
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global().async { [weak self] in
Thread.sleep(forTimeInterval: 3600)
self?.someProperty = 123
}
}
}
I used the Debug Memory Graph to figure out what is still keeping a reference to the instance of MyClass and this is what I get:
Why is there still a reference? What am I missing here?
UPDATE
So I was trying to figure out why it works with MyViewController but not when having another instance in between. Seems like inheriting from NSObject makes a difference. When I inherit MyClass from NSObject, deinit is called and once the long operation is finished, self is then correctly set to nil.
The question now is, in what way are capture lists in Swift related to NSObject?

Pushing Navigation Controller From Other Class in Swift

I have a Data Manager class that handles some JSON activities. When one action is completed, I want to push to the Navigation controller.
The FirstViewController calls a function on the DataManager Class
DataManager Class
class func extract_json(data:NSData) -> Bool {
//Success
FirstViewController().pushToValueView()
}
FirstViewController Class
func pushToValueView() {
println("Push to Value View")
navigationController?.pushViewController(ValueViewController(), animated: true)
}
In this example, println() is called but no push occurs. If I call pushToValueView() within FirstViewController e.g. self.pushToValueView() (For debug purposes), a push does occur.
Try calling this function on an existing instance of FirstViewController.
I think that in this example you try to push view controller to FirstViewController which is deallocated after exiting the method scope. That is why view controller doesn't appear
Unwrap the optional. Edit: You shouldn't call your vc from a model class btw, thats bad MVC, I'd use an observers rather and get notified when the closure is complete
if let nav = self.navigationController {
println("Push to Value View")
nav.pushViewController(ValueViewController(), animated: true)
}
You can modify your extract_json function so that it accepts a closure to be executed once the data has been extracted. The closure will contain the code to push the new view controller and it will execute in the context of the calling rather than called object:
In DataManager.swift -
func extractJSON(data:NSData, completion:() -> Void) {
//Do whatever
completion()
}
In FirstViewController.swift -
func getDataAndSegue() {
extractJSON(someData, completion:{
navigationController?.pushViewController(ValueViewController(), animated: true)
}
)
}

Resources