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

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()

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
}

Referencing item's initialized in viewDidLoad safely

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

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.

UIViewController initWithNibNamed:bundle: initialized two objects?

I have a UIViewController that I have had in a storyboard for a while with no problems. As my application grew, and I was using that view controller in more and more places, I realized that I should probably make it more portable, rather than have so many segues to it from hither and yon across the board. I've done splits like this before, so I did what I figured was logical here. I selected that view controller, cut it, and pasted into an empty .xib file. After changing each call to performSegueWithIdentifier to an init(nibName:bundle:) and presentViewController, I get a crash, with an object found unexpectedly nil in viewDidLoad()...
I set the value of this object after each init(...) call, just before presenting the view controller. The nil object is called from viewDidLoad(). This is a problem. I just set this, and now it's gone?!
I overrode the init(...) method, and found that self in init(nibName:bundle:) doesn't have the same memory address as self in viewDidLoad(). Also strange.
I overrode the other init() methods, and found that, after I call to present my view, my object is being instantiated again via init(coder:)! The self in here happens to be the exact self where my property is found nil!
The only reason I see for init(coder:) to be called at all is that I am loading my view from a .xib, but I thought this was handled in init(nibNamed:bundle:)? According to the docs, I do indeed get a call to init(coder:) if I'm loading from a storyboard, and doesn't touch the former... It also says that the nib isn't loaded until the controller's view is queried. If I understand it correctly, my view shouldn't get queried until I present the view. As the crash happens only when I present it, the issue likely stems from that.
I'm stuck here. I still need to get this contextual information to the view controller before it's presented. I've even tried making a proxy class to do the instantiating and property setting before presentation, but I still can't shake this second instance! I get one from init(nibName:bundle:), and another from init(coder:). Neither gets presented, and the latter gives me a nil object error. Any help at all in understanding why this is, and how I might work around this bug (feature?) would be much appreciated. Thank you!
Update:
On a whim, I decided to paste the view controller back into the storyboard, separate from the main hierarchy, and try instantiating it by its identifier. It worked! Not entirely sure how, but by George it worked! Now my question is this: Why?? What is so terribly evil and taboo about .xibs that Xcode and iOS won't tell me? I'm not a little flummoxed by this behavior. I'll keep trying with the .xib, if only to keep Xcode from yelling at me about entrance points...
I don't know what dark magic Xcode is doing, but here's two helper methods I wrote to easily instantiate any Storyboard VC - you just need the Storyboard name and VC identifier (optionally, otherwise will initial VC). By splitting up my VCs into many different Storyboards, I avoid dealing with xibs while still keeping things simple. One loads it into a nav controller of your choice, the other just returns it by itself:
struct StoryboardHelper {
///instantiates a VC with (optional) identifier viewController from storyboardName, pushes it to hierarcy of navigationController, and runs setup block on it, animated specifies whether the push is animated
internal static func showStoryboard(storyboardName: String, viewController: String?, navigationController: UINavigationController, animated: Bool = true, setup: (UIViewController) -> () ){
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let destinationVC = viewController != nil ? storyboard.instantiateViewControllerWithIdentifier(viewController!) : storyboard.instantiateInitialViewController()!
setup(destinationVC)
navigationController.pushViewController(destinationVC, animated: animated)
}
///instantiates and returns a VC with (optional) identifier viewController from storyboardName
internal static func instantiateViewControllerFromStoryboard(storyboardName: String, viewController: String?) -> UIViewController{
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
return viewController != nil ? storyboard.instantiateViewControllerWithIdentifier(viewController!) : storyboard.instantiateInitialViewController()!
}
}

Short lag when setting label text using storyboards

In my application, I have a mainViewController with some content on it. At some points, I load an overlay view controller from storyboard. The overlay view controller is smaller than the screen and is presented on top of the mainViewController. I initialize it the following way:
class MyOverlayViewController {
#IBOutlet var textLabel: UILabel!
#IBOutlet var countLabel: UILabel!
static let storyboard = UIStoryboard(name: "...", bundle: nil)
// Return a new view controller
class func newViewControllerWithData(data: AnyObject) -> UIViewController {
let vc = storyboard.instantiateViewControllerWithIdentifier("MyOverlayViewController") as! MyOverlayViewController
Timing.performAfterDelay(0) {
vc.titleLabel.text = data[...] // Load title label text
vc.countLabel.text = data[...] // Load count label text
}
return vc
}
}
I cannot set the text of the labels immediately in the method newViewControllerWithData, because that produces the following error: fatal error: unexpectedly found nil while unwrapping an Optional value. So the labels are nil when accessing them immediately in this method.
It seems like the two label outlets are not loaded immediately when the view controller is instantiated from storyboard, because this takes a (very) short time.
Therefore, I use my method Timing.performAfterDelay(0) which executes the code after the next run-loop cycle (it starts a timer with duration 0 and executes the code as callback). The code is (I have checked that) executed on the main thread.
The problem is the following:
Sometimes (not always, and not reproducible!), when loading the overlay view controller, for a fraction of a second the labels are empty (like I have defined them in storyboard) before they are showing the text.
So the user sees empty labels for a short moment before the actual data is loaded into the labels.
How can I fix this behavior?
Is it possible somehow to access the outlets immediately after instantiating the view controller from storyboard, without using Timing.performAfterDelay(0)?
Help would be appreciated.
Outlets are set after view is loaded i.e. when viewDidLoad gets called on the view controller. However, calling it directly like vc.viewDidLoad() will not work, you have to access the view controller's view like let dummyVariable = vc.view instead. Here's the code that force loads the view and then sets the label values.
class func newViewControllerWithData(data: AnyObject) -> UIViewController {
let vc = storyboard.instantiateViewControllerWithIdentifier("MyOverlayViewController") as! MyOverlayViewController
let _ = vc.view // force load the view
// now set your outlets as you please
vc.titleLabel.text = data[...] // Load title label text
vc.countLabel.text = data[...] // Load count label text
return vc
}
NOTE: This is not really a good practice though. MyOverlayViewController should be responsible for setting its label values instead of these being set from the outside. You could pass it the required data via a property or argument to a method, etc.

Resources