Setting translatesAutoresizingMaskIntoConstraints = false creates multiple objects that instantly gets deinitialized - swift

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.

Related

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.

RealityKit – Can't deallocate ARView

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.

Unable to access property of UITableView subclass

I am trying to test whether the method ReloadData() is called by an instance of UITableView when it's dataSource is updated.
I've created a subclass of UITableView called MockTableView. It has a bool called reloadDataGotCalled which is set to true when the overridden function reloadData() is called. I then try access that property from within my test class to test whether it is true.
However when I try to do so the compiler gives me the message that "Value of type 'UITableView' has no member 'reloadDataGotCalled'"
I'm not sure why it's doing that, because as far as I can see I've set that value to be of the type 'MockTableView' which should have that member?
// A ViewController that contains a tableView outlet that I want to test.
class ItemListViewController: UIViewController {
let itemManager = ItemManager()
#IBOutlet var tableView: UITableView!
#IBOutlet var dataProvider: (UITableViewDataSource & UITableViewDelegate & ItemManagerSettable)!
#IBAction func addItem(_ sender: UIBarButtonItem) {
if let nextViewController = storyboard?.instantiateViewController(identifier: "InputViewController") as? InputViewController {
nextViewController.itemManager = itemManager
present(nextViewController, animated: true, completion: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataProvider
tableView.delegate = dataProvider
dataProvider.itemManager = itemManager
}
}
// My test class
class ItemListViewControllerTest: XCTestCase {
var sut: ItemListViewController!
override func setUp() {
//Given
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "ItemListViewController")
sut = (viewController as! ItemListViewController)
//When
sut.loadViewIfNeeded()
}
// The test where I'm trying to assign sut.tableView to mockTableView
func test_TableView_IsReloadedWhenItemAddedToItemManger() {
let mockTableView = MockTableView()
sut.tableView = mockTableView
let item = ToDoItem(title: "Foo")
sut.itemManager.add(item)
sut.beginAppearanceTransition(true, animated: true)
sut.endAppearanceTransition()
XCTAssertTrue(sut.tableView.reloadDataGotCalled) // <- this is where I'm getting the compiler message "Value of type 'UITableView' has no member 'reloadDataGotCalled'"
}
}
// My mockTableView subclass in an extension of the ItemListViewControllerTests
extension ItemListViewControllerTest {
class MockTableView: UITableView {
var reloadDataGotCalled = false
override func reloadData() {
super.reloadData()
reloadDataGotCalled = true
}
}
}
I'm expecting that it should compile, and then the test should fail because I've not written the code to make it pass yet?
You have defined tableView instance in ItemListViewController as UITableView. So, you can't access the MockTableView's property with that instance.
You can only access the parent's properties from the children not the vice versa. If you still want to access the property you can try something like the snippet below.
XCTAssertTrue((sut.tableView as! MockTableView).reloadDataGotCalled)
Hope it helps.

Swift view appears to delay being loaded into window hierarchy

A very strange thing is happening, it appears my initial view controller loads up, performs its logic and all but the actual view itself isn't added to the hierarchy until everything else happens.
protocol OnboardDisplayLogic: class
{
func handleUserConfirmation(viewModel: OnboardModels.CheckForUser.ViewModel)
}
class OnboardViewController: UIViewController, OnboardDisplayLogic {
var interactor: OnboardInteractor?
var router: (NSObjectProtocol & OnboardRoutingLogic & OnboardDataPassing)?
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
//MARK: Setup
private func setup() {
let viewController = self
let interactor = OnboardInteractor()
let presenter = OnboardPresenter()
let router = OnboardRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
private func setupView() {
view.backgroundColor = UIColor.brown
}
//MARK: Routing
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let scene = segue.identifier {
let selector = NSSelectorFromString("routeTo\(scene)WithSegue:")
if let router = router, router.responds(to: selector) {
router.perform(selector, with: segue)
}
}
}
//MARK: View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupView()
interactor?.checkForUser()
}
func handleUserConfirmation(viewModel: OnboardModels.CheckForUser.ViewModel) {
if let username = viewModel.username {
print("Logged in user: \(username)")
router?.routeToUserFeed(segue: nil)
} else {
print("No user logged in")
router?.routeToLoginRegister(segue: nil)
}
}
The reason I think it's delayed is that although I've set the view background colour to brown, using breakpoints at the print("user not logged in") code I get that printed to console, because there is no user logged in as determined the Interactor, but view remains white. It's not until I step over and complete running all the code does the screen then become brown.
Therefore I'm getting the error
Warning: Attempt to present ... whose view is not in the window
hierarchy!
as something is preventing the view from being added until the very end.
Actual views aren't loaded until they enter the view controller lifecycle.
If you need to load the view to setup before it's added to a superview's hierarchy, just use the getter of the view (let _ = viewController.view).
UIViewControllers also have the loadView() method, check it out: Apple documentation.
We've also had these kind of problems with Clean Swift. The correct way of working would be to instantiate from the storyboard and just use normal segues/lifecycle, just as MVC does for Apple examples.
Instead of doing let viewControllerToPush = MyVCSublass(...), you should instantiate from storyboard let viewControllerToPush = storyboard?.instantiate... and use that view controller.

Having issues setting delegate with Observer Pattern

I'm trying to realize the Observer Pattern and I'm experiencing some difficulty as my delegate doesn't seem to be setting properly.
In my Main.storyboard I have a ViewController with a container view. I also have an input box where I'm capturing numbers from a number keypad.
Here's my storyboard:
I'm trying to implement my own Observer Pattern using a protocol that looks like this:
protocol PropertyObserverDelegate {
func willChangePropertyValue(newPropertyValue:Int)
func didChangePropertyValue(oldPropertyValue:Int)
}
My main ViewController.swift
class ViewController: UIViewController {
#IBOutlet weak var numberField: UITextField!
// observer placeholder to be initialized in implementing controller
var observer : PropertyObserverDelegate?
var enteredNumber: Int = 0 {
willSet(newValue) {
print("//Two: willSet \(observer)") // nil !
observer?.willChangePropertyValue(5) // hard coded value for testing
}
didSet {
print("//Three: didSet")
observer?.didChangePropertyValue(5) // hard coded value for testing
}
}
#IBAction func numbersEntered(sender: UITextField) {
guard let inputString = numberField.text else {
return
}
guard let number : Int = Int(inputString) else {
return
}
print("//One: \(number)")
self.enteredNumber = number // fires my property observer
}
}
My ObservingViewController:
class ObservingViewController: UIViewController, PropertyObserverDelegate {
// never fires!
func willChangePropertyValue(newPropertyValue: Int) {
print("//four")
print(newPropertyValue)
}
// never fires!
func didChangePropertyValue(oldPropertyValue: Int) {
print("//five")
print(oldPropertyValue)
}
override func viewDidLoad() {
super.viewDidLoad()
print("view loads")
// attempting to set my delegate
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
}
}
Here's what my console prints:
What's happening
As you can see when my willSet fires, my observer is nil which indicates that I have failed to set my delegate in my ObservingViewController. I thought I set my delegate using these lines:
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController") as! ViewController
print("//six \(pvc)")
pvc.observer = self
However, I must be setting my delegate incorrectly if it's coming back nil.
Question
How do I properly set my delegate?
You are calling into the storyboard to instantiate a view controller and setting it as the observer, however that instantiates a new instance of that view controller, it doesn't mean that it is referencing the one single "view controller" that is in the storyboard. ObservingViewController needs another way to reference the ViewController that has already been created.
So #Chris did reenforce my suspicions which helped me to figure out a solution for assigning my delegate to my view controller properly.
In my ObservingViewController I just need to replace the code in my viewDidLoad with the following:
override func viewDidLoad() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let vc = app.window?.rootViewController as! ViewController
vc.observer = self
}
Rather than creating a new instance of my view controller, I'm now getting my actual view controller.