Referencing item's initialized in viewDidLoad safely - ios

I ran into a bizarre bug earlier this week and wanted to follow up to see how to prevent the root cause of the issue.
Take the following code.
//*****************************
//MAINVIEWCONTROLLER CLASS CODE
//*****************************
//Some event happens that triggers me to want to load up TestViewController.
func showViewController(){
var testController = TestViewController()
testController.someMethod("Test1")
self.navigationController?.pushViewController(testController, animated: true)
}
//*****************************
//TESTVIEWCONTROLLER CLASS CODE
//*****************************
testView:TestView!
override func viewDidLoad(){
super.viewDidLoad()
testView = TestView()
...
}
func someMethod(someData:String){
testView.name = someData //AppCrashes here because testView might be nil.
...
}
So someMethod is getting fired before TestViewController has had the chance to go through and create the testView. I'm then getting a cannot unwrap an optional value because testView is nil and I'm accessing a property on it.
Whats strange is the application I'm running probably does this exact thing in 6 different places, and 5/6 are working perfectly fine, but 1/6 is now giving me this error. I'm guessing its because of the viewDidLoad not being guaranteed to fire immediately or complete before someMethod is executed, but why then is this not happening on all 6 of the use cases.
So my main questions are:
Why does this crash happen?
What is the best practice to avoid it.
Thanks! Thoughtful answers will get up-votes as always! Let me know if any more info would be helpful.

Basically never run code in the destination controller called from the source controller which involves UI elements. Create a property, set it in the source controller and assign the value to the UI element in viewDidLoad() of the destination controller, for example:
//*****************************
//MAINVIEWCONTROLLER CLASS CODE
//*****************************
//Some event happens that triggers me to want to load up TestViewController.
func showViewController(){
var testController = TestViewController()
testController.someData = "Test1"
self.navigationController?.pushViewController(testController, animated: true)
}
//*****************************
//TESTVIEWCONTROLLER CLASS CODE
//*****************************
testView:TestView!
var someData = ""
override func viewDidLoad(){
super.viewDidLoad()
testView = TestView()
testView.name = someData
...
}

viewDidLoad is called when the ViewController completes loading in preparation to be shown, e.g. when a segue takes place or when involved in a present.
As written your code shouldn't even compile since testView is optional, but you have two options. Use optionals (in which case the view may not get the information if not called after viewDidLoad, but it won't crash) or store the passed information and update your view in viewDidLoad or viewWillAppear.
Something you might want to be aware of is viewIfLoaded

You can force the viewDidLoad method with with:
var testViewController = TestViewController()
_ = testViewController.view
testViewController.someMethod("Test")
Initializing the ViewController doesn't automatically call viewDidLoad

Related

How to prevent timer reset using pushViewController method?

I'm trying to keep a timer running even if I switch view controllers. I played around with the Singleton architecture, but I don't quite get it. Pushing a new view controller seems a little easier, but when I call the below method, the view controller that is pushed is blank (doesn't look like the view controller that I created in Storyboards). The timer view controller that I'm trying to push is also the second view controller, if that changes anything.
#objc func timerPressed() {
let timerVC = TimerViewController()
navigationController?.pushViewController(timerVC, animated: true)
}
You need to load it from storyboard
let vc = self.storyboard!.instantiateViewController(withIdentifier: "VCName") as! TimerViewController
self.navigationController?.pushViewController(timerVC, animated: true)
Not sure if your problem is that your controller is blank or that the timer resets. Anyway, in case that you want to keep the time in the memory and not deallocate upon navigating somewhere else I recommend you this.
Create some kind of Constants class which will have a shared param inside.
It could look like this:
class AppConstants {
static let shared = AppConstants()
var timer: Timer?
}
And do whatever you were doing with the timer here accessing it via the shared param.
AppConstants.shared.timer ...
There are different parts to your question. Sh_Khan told you what was wrong with the way you were loading your view controller (simply invoking a view controller’s init method does not load it’s view hierarchy. Typically you will define your view controller’s views in a storyboard, so you need to instantiate it from that storyboard.)
That doesn’t answer the question of how to manage a timer however. A singleton is a good way to go if you want your timer to be global instead of being tied to a particular view controller.
Post the code that you used to create your singleton and we can help you with that.
Edit: Updated to give the TimeManager a delegate:
The idea is pretty simple. Something like this:
protocol TimeManagerDelegate {
func timerDidFire()
}
class TimerManager {
static let sharedTimerManager = TimerManager()
weak var delegate: TimeManagerDelegate?
//methods/vars to manage a shared timer.
func handleTimer(timer: Timer) {
//Put your housekeeping code to manage the timer here
//Now tell our delegate (if any) that the timer has updated.
//Note the "optional chaining" syntax with the `?`. That means that
//If `delegate` == nil, it doesn't do anything.
delegate?.timerDidFire() //Send a message to the delegate, if there is one.
}
}
And then in your view controller:
//Declare that the view controller conforms to the TimeManagerDelegate protocol
class SomeViewController: UIViewController, TimeManagerDelegate {
//This is the function that gets called on the current delegate
func timerDidFire() {
//Update my clock label (or whatever I need to do in response to a timer update.)
}
override func viewWillAppear() {
super.viewWillAppear()
//Since this view controller is appearing, make it the TimeManager's delegate.
sharedTimerManager.delegate = self
}

is there a shortcut to know, when I'm running my app, which view controller am I in?

I'm working on an application that already exists, and I want to run the app and know in the output which view controller am I in, without going to every class and viewController and printing its name. Is there a shortcut or something that can help me ??
I've tried printing in view did load in every ViewController
like
print("view did load : MainViewController")
but I found this wasting of time because I have so many viewControllers.
I think about debugging but I really don't know what it does or how.
You could swizzle (exchange the implementation of) UIViewController.viewDidAppear so it prints the class name. First define your swizzled implementation:
extension UIViewController {
#objc func printingViewDidAppear(_ animated: Bool) {
print(String(describing: type(of: self)))
// Call the original implementation. This looks like recursion but isn't
printingViewDidAppear(animated)
}
}
Then exchange the default implementation with your implementation like this:
let originalViewDidAppear: Method = class_getInstanceMethod(UIViewController.self, #selector(UIViewController.viewDidAppear(_:)))!
let swizzledViewDidAppear: Method = class_getInstanceMethod(UIViewController.self, #selector(UIViewController.printingViewDidAppear(_:)))!
method_exchangeImplementations(originalViewDidAppear, swizzledViewDidAppear)
That way you wouldn't need to change any of your existing view controllers.
You must make sure to call the original implementation at the end of the swizzled implementation. It looks like a recursive call but it isn't. Hope that helps.
I'm not sure this is possible. What you can do, however, to make it a little simpler, is adding this extension to UIViewController:
extension UIViewController {
var className: String {
return String(describing: type(of: self))
}
}
Then, in viewDidLoad, print it:
print("loaded \(self.className)")

Ensure that property observer didSet manipulates User Interface after viewDidLoad

I am working on an open source tutorial using MVVM, Coordinators and RxSwift. I am constructing all the viewcontrollers and models in the coordinator. Controller has a strong reference to viewmodel and when a viewmodel is set, I would like to perform some UI related actions(using property observer didSet). The problem I am facing is that didSet is called before viewDidLoad causing a crash.
Stripped down version of ViewController:
class MessageVC: UIViewController {
var viewModel: MessageViewModel! {
didSet {
manipulateUI() // crashes
}
}
override func viewDidLoad() {
super.viewDidLoad()
manipulateUI() // works fine if setup is correct in coordinator
}
Coordinator stripped down version:
extension AppCoordinator {
convenience init() {
let rootVC = MessageVC() // actual construction from storyboard
let messages = Message.getMessages()
rootVC.viewModel = MessageViewModel(withMessage: messages)
}
My concern is that even though calling manipulateUI in viewDidLoad is working for me currently, the app will crash if I forget to set the viewModel from my co-ordinator making me think that I am using a fragile architecture. I really like updating userinterface from didSet but it is called before viewDidLoad.
I know it is a simple problem but from architecture standpoint it seems fragile. Any suggestions, improvements and comments are appreciated a lot.
I wont say that cases like this can define wether you are dealing with fragile architecture or not because view controllers has their own life cycle which differs a lot from other objects life cycle. Anyway you can easily avoid crashes here using different approaches. For example :
Approach 1:
Put a guard statement at the very beginning of your manipulateUI function so this function wont manipulate UI until both view is loaded and model is set. Then call this function on viewDidLoad method and when viewModel is set:
func manipulateUI(){
guard let viewModel = self.viewModel , isViewLoaded else {
return
}
//continue manipulation here
}
Approach 2:
Since you are not sure wether view is loaded when you set the model and don't know if views are initialized yet, you can access the views as optional properties in manipulateUI function:
func manipulateUI(){
self.someLabel?.text = self.viewModel.someText
//continue manipulation here
}
Approach 3:
Since you are using RxSwift you can always register an observer for view controller's isViewLoaded property and set the data source after you are sure that view is loaded
Crash happens because at this point
rootVC.viewModel = MessageViewModel(withMessage: messages)
view controller is not initialized.
It won't work the way you're trying to accomplish, you have to call manipulateUI() inside viewDidLoad.

Why does the view property have to be assigned to a variable?

In the following code if I comment out the variable assigned to the view property, the tests fail. The line I'm referring to is:
_=sut.view
However when that line of code is uncommented, the tests pass. Why is it even necessary?
Here is the full unit test:
import XCTest
#testable import ToDo
class ItemListViewControllerTests: XCTestCase {
var sut:ItemListViewController!
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
sut = storyboard.instantiateViewControllerWithIdentifier("ItemListViewController") as! ItemListViewController
_=sut.view
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func test_TableViewIsNotNilAfterViewDidLoad(){
XCTAssertNotNil(sut.tableView.dataSource)
XCTAssertTrue(sut.tableView.dataSource is ItemListDataProvider)
}
func testViewDidLoad_ShouldSetTableViewDelegate(){
XCTAssertNotNil(sut.tableView.delegate)
XCTAssertTrue(sut.tableView.delegate is ItemListDataProvider)
}
func testViewDidLoad_ShouldSetDelegateAndDataSourceToSameObject(){
XCTAssertEqual(sut.tableView.dataSource as? ItemListDataProvider, sut.tableView.delegate as? ItemListDataProvider)
}
}
View controllers don't load their view until the first time the view property is accessed, so assigning the view to a variable will load it.
If the view isn't loaded then none of the outlets will be hooked up so sut.tableView will be nil and your tests will fail.
The view of a controller is loaded lazily the first time you are accessing it (automatically calling UIViewController.loadView and then UIViewController.viewDidLoad).
If you access this property and its value is currently nil, the view controller automatically calls the loadView method and returns the resulting view.
Because accessing this property can cause the view to be loaded automatically, you can use the isViewLoaded method to determine if the view is currently in memory. Unlike this property, the isViewLoaded property does not force the loading of the view if it is not currently in memory.
(from UIViewController.view)
Loading the controller view means that all its subviews are loaded and connected to outlets, therefore if you don't load the view, the tableView outlet will be nil.
Assigning to _ is there only to silence the compiler warning about unused result. On iOS 9 and higher you can achieve the same using sut.loadViewIfNeeded()

UIView doesn't change at runtime

I've had this working in other variations but something seems to elude me in the change from objective-c to swift as well as moving some of the setup into it's own class.
So i have:
class ViewController: UIViewController, interfaceDelegate, scrollChangeDelegate{
let scrollControl = scrollMethods()
let userinterface = interface()
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
}
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
}
}
This sets everything up correctly but the problem occurs when I change loadMenu() at runtime. So if the user calls loadMenu("AnotherMenu") it won't change the UIView. It will call the right functions but it won't update the view. Although if I call loadMenu("AnotherMenu") at the start, the correct menu will display. Or if I call loadMenu("Start") and then loadMenu("AnotherMenu") then the menu displayed will be "AnotherMenu". As in:
override func viewDidLoad(){
super.viewDidLoad()
loadMenu("Start")
loadMenu("AnotherMenu")
}
When I list all the subviews each time loadMenu() is called, they look correct. Even during runtime. But the display is not updated. So something isn't getting the word. I've tried disabling Auto Layout after searching for similar issues but didn't see a difference.
Try adding setNeedsDisplay() to loadMenu
Eg
func loadMenu(menuName: String) {
userinterface.delegate = self
userinterface.scrollDelegate = self
userinterface.removeFromSuperview() //no impact
scrollControl.removeFromSuperview() //no impact
userinterface.configureView(menuName)
view.addSubview(scrollControl)
scrollControl.addSubview(userinterface)
view.setNeedsDisplay()
}
setNeedsDisplay() forces the view to reload the user interface.
I didn't want to post the whole UIView class as it is long and I thought unrelated. But Dan was right that he would need to know what was going on in those to figure out the answer. So I created a dummy UIView class to stand in and intended to update the question with that. I then just put a button on the ViewController's UIView. That button was able to act on the view created by the dummy. So the problem was in the other class. Yet it was calling the methods of the ViewController and seemingly worked otherwise. So then the issue must be that its acting on an instanced version? The way the uiview class worked, it uses performSelector(). But in making these methods into their own class, I had just lazily wrote
(ViewController() as NSObjectProtocol).performSelector(selector)
when it should have been
(delegate as! NSObjectProtocol).performSelector(selector)
so that was annoying and I wasted the better part of a day on that. But thanks again for the help.

Resources