RealityKit – Can't deallocate ARView - swift

I'm using a ARView to show AR content in my app, but on dismissing the view controller, the memory is not completely deallocated.
I found this question, but I had no luck with the answer.
The memory consumption looks like this:
The entire codebase is very simple:
I've got a button that on pressing it, executes this:
let vc = SecondViewController()
self.present(vc, animated: true, completion: {
print("done")
})
and SecondViewController is a simple as this:
class SecondViewController: UIViewController {
var arView: ARView?
override func viewDidLoad() {
super.viewDidLoad()
self.arView = ARView(frame: self.view.frame)
self.view.addSubview(arView!)
arView!.automaticallyConfigureSession = true
}
deinit {
self.arView?.session.pause()
self.arView?.session.delegate = nil
self.arView?.scene.anchors.removeAll()
self.arView?.removeFromSuperview()
self.arView?.window?.resignKey()
self.arView = nil
}
}
But on dismissing, as can be seen in the memory graph, the memory is not deallocated fully.

Related

Testing tableview.reloadData()

while using a MockTableView this code still not calling reloadData() from the mock,
please i wanna know what is wrong here.
following this book: Test-Driven IOS Development with Swift 4 - Third Edition
page 164, i was as an exercise
full code repo - on github
ItemListViewController.swift
import UIKit
class ItemListViewController: UIViewController, ItemManagerSettable {
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate &
ItemManagerSettable)!
var itemManager: ItemManager?
override func viewDidLoad() {
super.viewDidLoad()
itemManager = ItemManager()
dataProvider.itemManager = itemManager
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController =
storyboard?.instantiateViewController(
withIdentifier: "InputViewController")
as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
}
ItemListViewControllerTest.swift
import XCTest
#testable import ToDo
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
var addButton: UIBarButtonItem!
var action: Selector!
override func setUpWithError() throws {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier:
"ItemListViewController")
sut = vc as? ItemListViewController
addButton = sut.navigationItem.rightBarButtonItem
action = addButton.action
UIApplication.shared.keyWindow?.rootViewController = sut
sut.loadViewIfNeeded()
}
override func tearDownWithError() throws {}
func testItemListVC_ReloadTableViewWhenAddNewTodoItem() {
let mockTableView = MocktableView()
sut.tableView = mockTableView
guard let addButton = sut.navigationItem.rightBarButtonItem else{
XCTFail()
return
}
guard let action = addButton.action else{
XCTFail()
return
}
sut.performSelector(onMainThread: action, with: addButton, waitUntilDone: true)
guard let inputViewController = sut.presentedViewController as?
InputViewController else{
XCTFail()
return
}
inputViewController.titleTextField.text = "Test Title"
inputViewController.save()
XCTAssertTrue(mockTableView.calledReloadData)
}
}
extension ItemListViewControllerTest{
class MocktableView: UITableView{
var calledReloadData: Bool = false
override func reloadData() {
calledReloadData = true
super.reloadData()
}
}
}
You inject a MockTableview Then you call loadViewIfNeeded(). But because this view controller is storyboard-based and the table view is an outlet, the actual table view is loaded at this time. This replaces your MockTableview.
One solution is:
Call loadViewIfNeeded() first
Inject the MockTableview to replace the actual table view
Call viewDidLoad() directly. Even though loadViewIfNeeded() already called it, we need to repeat it now that we have a different tableview in place.
Another possible solution:
Avoid MockTableview completely. Continue to use a real table view. You can test whether it reloads data by checking whether the number of rows matches the changed data.
Yet another solution:
Avoid storyboards. You can do this with plain XIBs (but these lack table view prototype cells) or programmatically.
By the way, I see all your tearDownWithError() implementations are empty. Be sure to tear down everything you set up. Otherwise you will end up with multiple instances of your system under test alive at the same time. I explain there here: https://qualitycoding.org/xctestcase-teardown/

Adding a completion handler to UIViewPropertyAnimator in Swift

I'm trying to get a property animator to start animation when a View Controller is presented.
Right now the animation is playing however the UIViewPropertyAnimator doesn't respond to the completion handler added to it.
UIVisualEffectView sub-class.
import UIKit
final class BlurEffectView: UIVisualEffectView {
deinit {
animator?.stopAnimation(true)
}
override func draw(_ rect: CGRect) {
super.draw(rect)
effect = nil
animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in
self.effect = theEffect
}
animator?.pausesOnCompletion = true
}
private let theEffect: UIVisualEffect = UIBlurEffect(style: .regular)
var animator: UIViewPropertyAnimator?
}
First View controller
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func doSomething(_ sender: UIButton) {
let vc = storyboard?.instantiateViewController(identifier: "second") as! SecondVC
vc.modalPresentationStyle = .overFullScreen
present(vc, animated: false) { //vc presentation completion handler
//adding a completion handler to the UIViewPropertyAnimator
vc.blurView.animator?.addCompletion({ (pos) in
print("animation complete") //the problem is that this line isn't executed
})
vc.blurView.animator?.startAnimation()
}
}
}
Second view controller
import UIKit
class SecondVC: UIViewController {
#IBOutlet weak var blurView: BlurEffectView!
override func viewDidLoad() {
super.viewDidLoad()
}
}
Here the UIViewPropertyAnimator completion handler is added after the Second View Controller(controller with visual effect view) is presented. I have tried moving the completion handler to different places like viewDidLoad and viewDidAppear but nothing seems to work.
This whole thing seems incorrectly designed.
draw(_ rect:) is not the place to initialize your animator*, my best guess at what's happening is that vc.blurView.animator? is nil when you try to start it (have you verified that it isn't?).
Instead, your view class could look like this**:
final class BlurEffectView: UIVisualEffectView {
func fadeInEffect(_ completion: #escaping () -> Void) {
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.5, delay: 0, options: []) {
self.effect = UIBlurEffect(style: .regular)
} completion: { _ in
completion()
}
}
}
And you would execute your animation like this:
present(vc, animated: false) { //vc presentation completion handler
vc.blurView.fadeInEffect {
// Completion here
}
}
*draw(_ rect:) gets called every time you let the system know that you need to redraw your view, and inside you're supposed to use CoreGraphics to draw the content of your view, which is not something you're doing here.
**Since you're not using any of the more advanced features of the property animator, it doesn't seem necessary to store it in an ivar.
The problem is that you are setting pausesOnCompletion to true. This causes the completion handler to not be called.
If you actually need that to be set to true, you need to use KVO to observe the isRunning property:
// property of your VC
var ob: NSKeyValueObservation?
...
self.ob?.invalidate()
self.ob = vc.blurView.animator?.observe(\.isRunning, options: [.new], changeHandler: { (animator, change) in
if !(change.newValue!) {
print("completed")
}
})
vc.blurView.animator?.startAnimation()
And as EmilioPelaez said, you shouldn't be initialising your animator in draw. Again, if you actually have a reason for using pausesOnCompletion = true, set those in a lazy property:
lazy var animator: UIViewPropertyAnimator? = {
let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in
self.effect = self.theEffect
}
anim.pausesOnCompletion = true
return anim
}()
self.effect = nil could be set in the initialiser.

How to show a specific UIViewController when GKGameCenterViewController is dismissed?

I am presenting a GKGameCenterViewController in an SKScene that inherits from the following protocol.
protocol GameCenter {}
extension GameCenter where Self: SKScene {
func goToLeaderboard() {
let vc = GKGameCenterViewController()
vc.gameCenterDelegate = GameViewController()
vc.viewState = .leaderboards
vc.leaderboardIdentifier = "leaderboard"
view?.window?.rootViewController?.present(vc, animated: true, completion: nil)
}
}
While the GKGameCenterViewController shows up perfect, when I try to dismiss by clicking the X in the top right corner, nothing happens. I assume this is because the reference to my original GameViewController has been deallocated. How can I get this dismissal to work?
According to Apple's Documentation:
Your delegate should dismiss the Game Center view controller. If your game paused any gameplay or other activities, it can restart those services in this method.
This means you need to implement the gameCenterViewControllerDidFinish method in your delegate and dismiss the gameCenterViewController yourself.
You should have something like this in your GameViewController()
func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
gameCenterViewController.dismiss(animated: true, completion: nil)
}
In order to present the GKGameCenterViewController on a SKScene, I needed to find the currently displayed UIViewController reference and set this as the delegate. Here is the code I used and it works:
protocol GameCenter {}
extension GameCenter where Self: SKScene {
func goToLeaderboard() {
var currentViewController:UIViewController=UIApplication.shared.keyWindow!.rootViewController!
let vc = GKGameCenterViewController()
vc.gameCenterDelegate = currentViewController as! GKGameCenterControllerDelegate
vc.viewState = .leaderboards
vc.leaderboardIdentifier = "leaderboard"
currentViewController.present(vc, animated: true, completion: nil)
}
}

Switching from a Spritekit Scene back to the storyboard

Simply put, I want to go from a spritekit Scene to a view in the Main Storyboard. It's easy to go from the main storyboard to a spritekit scene in swift. But I can't figure out how to go back to the storyboard. Thanks for the help. Cheers.
Initial viewController: an empty viewController with a button to present the GameViewController
GameViewController: the typical GameViewController of the "Hello World" Sprite-kit template. (This is a simplified version of the two scripts as of course you will have more code in yours, however, for the purpose of sharing what I did, this is easier)
My Goal: I wanted to present the first viewController from my SKScene game with the correct deallocation of my scene.
Description: To obtain the result I've extended the SKSceneDelegate class to build a custom protocol/delegate that make the transition from the GameViewController to the first initial controller (main menu). This method could be extended to other viewControllers of your game. This delegate is made use of in the return to main menu function. Make sure to put this function before you call the class for your spritekit script.
The two scripts are shown below. Hope this helps anybody else who had my question.
UIViewController:
import UIKit
import SpriteKit
class GameViewController: UIViewController,TransitionDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
if let scene = SKScene(fileNamed: "GameScene") {
scene.scaleMode = .aspectFill
scene.delegate = self as TransitionDelegate
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
func returnToMainMenu(){
let appDelegate = UIApplication.shared.delegate as! AppDelegate
guard let storyboard = appDelegate.window?.rootViewController?.storyboard else { return }
if let vc = storyboard.instantiateInitialViewController() {
print("go to main menu")
self.present(vc, animated: true, completion: nil)
}
}
}
Game Script:
import SpriteKit
protocol TransitionDelegate: SKSceneDelegate {
func returnToMainMenu()
}
class GameScene: SKScene {
override func didMove(to view: SKView) {
self.run(SKAction.wait(forDuration: 2),completion:{[unowned self] in
guard let delegate = self.delegate else { return }
self.view?.presentScene(nil)
(delegate as! TransitionDelegate).returnToMainMenu()
})
}
deinit {
print("\n THE SCENE \((type(of: self))) WAS REMOVED FROM MEMORY (DEINIT) \n")
}
}

Setting translatesAutoresizingMaskIntoConstraints = false creates multiple objects that instantly gets deinitialized

This is my code:
import UIKit
class MyView: UIView {
var nextView: NextView?
private var button: UIButton! = UIButton()
init(){
super.init(frame: CGRect.zero)
commonLoad()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonLoad()
}
private func commonLoad() {
addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false //comment this on and off to see the change
}
override func layoutSubviews() {
super.layoutSubviews()
nextView = NextView(view: self)
}
deinit {
print("deinit my view")
}
}
class NextView {
private weak var view: UIView?
init(view: UIView){
self.view = view
guard let v = self.view else { fatalError() }
let anotherSubView = UIView()
v.addSubview(anotherSubView)
}
deinit {
print("Deinit next view")
}
}
class Test: UIViewController{
override func viewDidAppear(_ animated: Bool) {
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { (_) in
let storyBoard = UIStoryboard(name: "Main", bundle:nil)
let nextViewController = storyBoard.instantiateViewController(withIdentifier: "otherVC") as! otherVC
UIApplication.shared.keyWindow?.rootViewController = nextViewController
}
}
}
Notice the comment when changing the value of translatesAutoresizingMaskIntoConstraints from the buttn. Changing this value will get this weird behaviour.
This is my log after adding a MyView to the storyboard in class Test:
Deinit next view //<-- Instant called, WHY?!
deinit my view //<-- after 1 second, good
Deinit next view //<-- after 1 second, good, but why did it created another object of NextView?
When commenting translatesAutoresizingMaskIntoConstraints off, the first printlog disappears. Why does changing the value of translatesAutoresizingMaskIntoConstraints instantly creates and nils the object NextView?
Okay, first of all you need to take a look at translatesAutoresizingMaskIntoConstraints documentation from Apple. It says:
By default, the property is set to true for any view you programmatically create.
Now come to your example.
When you said commenting translatesAutoresizingMaskIntoConstraints off, it's ambiguous. You should either say comment/uncomment or use value of true/false. It would be easier to think.
Consider these two cases:
translatesAutoresizingMaskIntoConstraints = true(by default, when creating from code), system assumes that you yourself are handling the framing of the view and propagates an extra pass to the layoutSubviews() in case you need to change the frame of any of your subviews.
translatesAutoresizingMaskIntoConstraints = false. System gets to know that your sub views will have frame calculated dynamically and for that reason you don't need any chance to re-frame your subviews. So the system doesn't propagate any extra pass to layoutSubviews()
Now you need to know when deinit gets called. For the simplest case, if any object is initialized the second time, the first object's deinit gets called. You should have get your answer by now.
Let me clear you up:
When you used translatesAutoresizingMaskIntoConstraints = true (or
if you even didn't use) your layoutSubviews() is called twice. As a
result you initialized NextView twice. When initializing the
NextView second time, the first instance's deinit gets called.
When you used translatesAutoresizingMaskIntoConstraints = false
your layoutSubviews() is called once. Hence no deinit gets
called.