I'm having trouble comprehending how the view property of UIViewController calls the viewDidLoad() method. It doesn't make sense to but I'd like to understand what's happening under the hood. I'm sure this is well explained in the Swift programming guide or maybe even in Apple's reference guide for UIViewController but right now is too verbose to quite understand. If it is explained in the Swift programming guide, I'm not sure of the correct term to research it further or how this process works. Maybe computed property? However from what I've learned about computed properties is that a computed property does some kind of logic in order to set its variable to a new value or maybe even the initial value. What's troubling me is understanding the concept of how a property calls a function in it's class? Most specifically the view property in UIViewController that calls the viewDidLoad method.
Here is my code that helped me stumble across this:
func test_OnViewDidLoad_tableViewIsSet(){
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "ItemListViewController")
let sut = viewController as! ItemListViewController
_ = sut.view
XCTAssertNotNil(sut.tableView)
}
Here is my subclassed UIViewController:
import UIKit
class ItemListViewController: UIViewController {
var tableView: UITableView?
override func viewDidLoad() {
tableView = UITableView()
}
}
Here's a rough outline of what is likely happening (we don't have the source code to UIViewController (which is written in Objective-C)).
class UIViewController: UIResponder {
private var _view: UIView!
var view: UIView! {
get {
if _view == nil {
loadView()
if _view != nil {
viewDidLoad()
}
}
return _view
}
set {
_view = newValue
}
}
}
I'm sure there is more to it but this should give you a rough idea how loadView and viewDidLoad end up being called simply by accessing the view property.
Related
i’m working in swift and i’m trying to use the .frames to check if 2 objects of type CGRect intersect.
i have my View Controller Class and a CircleClass, the CircleClass creates a circle that has gesture recognition so i can drag the circles that i create where i want to, now i want to add the option that if at the end of the drag the subview intersects my trashimageView (image view that will always be in the low-right corner of the view for all devices it's like a trashcan) it can delete the circle or subView.
the problem is that when i try to call trashImageView.frame in a function “deleteSubView” that i’ve created in the View Controller i get nil and my app crashes.
But if the IBOutlet is in the VC and my function is defined in my VC, also i can call the trashImageView.frame (CGRect Value) in the viewDidLoad and there is fine, but not in my function, why do i get nil for this value??
class ViewController: UIViewController {
#IBOutlet var trashImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
//here i can print the CGRect value just fine
print("my imageView init: \(trashImageView.frame)")
}
func deleteSubView(subView: UIView){
// Here i get nil from the trashImageView.frame
if (subView.frame.intersects(trashImageView.frame)) {
print("intersection")
subView.removeFromSuperview()
}
}
}
i've checked that the Nil value is from the 'trashImageView.frame' and that the connection with the storyboard is good.
i call the function ‘delete subView’ from another class but should that matter? i don’t understand what is the error here, why do i get nil? help please.
Since your UIViewController is declared and instantiated using storyboard my guess is that you are creating the view controller using it's no arg initializer, i.e.: let controller = MyController() if you must create an instance of the controller programmatically do so by obtaining a reference to the Storyboard that contains the controller, i.e like this:
NOTE: Here I'm using "MyController" as the name of the class and the identifier that has been set in the storyboard.
func createMyController() -> MyController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "MyController")
return controller as! MyController
}
I'd also add a guard for view load state in your deleteSubview(:subView) method, so something like this:
func deleteSubView(subView: UIView) {
guard isViewLoaded else { return }
// Here i get nil from the trashImageView.frame
if (subView.frame.intersects(trashImageView.frame)) {
print("intersection")
subView.removeFromSuperview()
}
}
My sender class for delegation:
import UIKit
protocol tapDelgation:class {
func tapConfirmed(message:String)
}
class ViewController: UIViewController {
weak var delegate:tapDelgation?
#IBAction func deligateSenderAction(_ sender: Any) {
var data = "hello world"
print(data)
self.delegate?.tapConfirmed(message: data)
}
}
My reciever class:
import UIKit
class NextViewController: UIViewController {
weak var vc:ViewController? = ViewController()
override func viewDidLoad() {
super.viewDidLoad()
vc?.delegate = self
}
}
extension NextViewController : tapDelgation {
func tapConfirmed(message: String) {
print(message)
}
}
What is expected: A button on sender vc is pressed and from reciever vc a console print would be popped. But in vain, nothing happens. Does any one know why it is happening? If it is not possible then why?
It looks like a memory management problem to me.
First problem: Creating a view controller with a default initializer like ViewController() is almost never the right thing to do. because it won't have any view contents.
You don't explain how your NextViewController and your ViewController get created and displayed.
It looks like NextViewController has a weak reference to ViewController, and ViewController's delegate point is also weak (delegate references should almost always be weak.)
This line:
weak var vc:ViewController? = ViewController()
Will cause NextViewController to create an instance of ViewController that isn't owned by anybody, so it will immediately be deallocated and the vc variable will go back to being nil. By the time you get to NextViewController's viewDidLoad, vc will be nil, so the optional binding in the line vc?.delegate = self won't do anything.
NextViewController's vc variable should almost certainly be a strong reference, not weak, but you don't show how ViewController ever gets displayed to the screen, so it isn't clear what you're trying to do.
weak var vc:ViewController? = ViewController()
Remove weak if you don't set the vc somewhere else and any other instance doesn't keep a strong reference to it.
If there is another instance with a strong reference, please share the related code.
The answer from the https://stackoverflow.com/users/205185/duncan-c is totally correct unless there is any other code which affects the presentation of the NextViewController and reference to the vc: ViewController
I changed viewController to SenderViewController but no luck and Sender and receiver is connected via navigation controller. i.e. If i press a button on sender a recieve comes via push transition. my aim was to since it is triggered an IBAction then the second view controller would implements the tap confirmed function. thanks for your answer. Learned a lot :)
Due to this comment, you need to implement prepareForSegue() method in your ViewController (original one) and set the vc property of the "next" view controller there instead of = ViewController() in the "next" make the extension on the ViewController:
extension ViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let nextController = segue.destinationViewController as! NextViewController
nextController.vc = self
}
}
Explanation based on the comment:
You get a new instance of the NextViewController with the new instance of the ViewController instantiated on its init (instead of passing the original instance of ViewController to it). That's where you can ge a strange behaviour with delegation.
weak var vc:ViewController? = ViewController()
Remove weak for vc it will release the view controller memory after disappear
I have a parent viewcontroller hosting 3 container viewcontrollers.
At certain points I need to pass data from one container viewcontroller to another container viewcontroller and thought I could accomplish this through the delegation pattern. However, I can't seem to figure out why the delegate is not triggered and the receiving container viewcontroller doesn't receive any data.
Can't seem to spot what's potentially wrong with the way I've set it up. If there's a recommended way to pass data between the containers, I'm all ears as well!
Below is a code summary on the setup:
class ParentViewController: UIViewController {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let firstContainerVC = segue.destination as? FirstContainerVC {
//....
}
if let secondContainerVC = segue.destination as? SecondContainerVC {
//....
}
}
protocol Delegate {
func passX(a: String?)
func passY(b: String?)
}
}
class FirstContainerVC: UIViewController {
var delegate: Delegate?
if isTrue {
delegate.passX(a: "TestOne")
} else {
delegate.passY(b: "TestTwo")
}
}
class SecondContainerVC: UIViewController, Delegate {
override func viewDidLoad() {
let firstVC = self.storyboard?.instantiateViewController(withIdentifier: "firstContainer") as! FirstContainerVC
firstVC.delegate = self
}
func passX(a: String?) {
//if let a = a....
}
func passY(b: String?) {
//if let b = b....
}
}
Unfortunately, I don't know how the dragging and dropping works in Xcode, I do everything in code. However, when your parent view controller instantiates another view controller, just set the parent as the container's delegate.
Create the protocol:
protocol SomeProtocol: AnyObject {
func passX(a: String?)
func passY(b: String?)
}
And the containers will have delegates of type that protocol:
class FirstContainerVC: UIViewController {
weak var delegate: SomeProtocol?
}
class SecondContainerVC: UIViewController {
weak var delegate: SomeProtocol?
}
The parent must conform to the protocol so that it can become the delegate. Then when you instantiate the containers (which you must only do once in this scenario), set self as their delegates:
class ParentViewController: UIViewController, SomeProtocol {
// make the containers instance properties so that you
// can access them from the protocol methods
weak var firstContainerVC = FirstContainerVC()
weak var secondContainerVC = SecondContainerVC()
// set the delegates at some point
func setDelegates() {
firstContainerVC?.delegate = self
secondContainerVC?.delegate = self
}
func passX(a: String?) {
guard let a = a else {
return
}
secondContainerVC?.getFromFirst(a: a)
}
func passY(b: String?) {
//
}
}
Then when you want to go from first to second, go through the delegate from the first container to the parent, and from the parent to the second container.
class FirstContainerVC: UIViewController {
weak var delegate: SomeProtocol?
func sendToSecond() {
delegate?.passX(a: "slick")
}
}
class SecondContainerVC: UIViewController {
weak var delegate: SomeProtocol?
func getFromFirst(a: String) {
print(a)
}
}
This is a somewhat crude example. You should code your implementation how you feel most comfortable (i.e. gracefully unwrapping, how/where you instantiate, etc.). Also, if all of the view controllers are permanent view controllers (meaning that they are never deallocated), no need to make them weak var. However you do it, the concepts are all the same. The parent is the delegate for the containers, and the containers communicate with each other through the parent.
Some people may suggest using notification observers or singletons for the containers to communicate with each other, but I find that to be overkill when you have a parent right there.
I agree with #slickdaddy that your second container view controller has no business instantiating your first container view controller. I suspect you already had a first container VC and now you have 2.
To answer your question about the best way to pass the data, your container view controllers should know nothing about the parent view controller. Through either delegation or a callback that the parent registers, the data should go to the parent and the parent should then route it down to the other interested contained view controllers.
Keep knowledge of your hierarchy flowing "downwards". In other words, contained or owned VCs should not know anything about their owners or parents. It'll help with reuse, organization, etc..
Also consider another approach: passing the same model object into each contained view controller.
And finally, my preferred approach is letting each view controller (or actually its view model if doing MVVM) reach for a DependencyManager singleton where such model objects live. If done right, unit tests can still have full control by injecting mock models into the DependencyManager.
I have a custom class segmented control that is communicating with a view controller every time a different item in the segmented control is selected. I'm able to pass data to the view controller just fine.
#IBDesignable class SegmentedControlLeft: UIControl {
var selectedIndex: Int = 0 {
didSet {
displayNewSelectedIndex()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let messagesViewController: MessagesViewController = storyboard.instantiateViewController(withIdentifier: "PurpleVC") as! MessagesViewController
messagesViewController.animateViews(selectedIndex: selectedIndex)
}
}
...some more code
}
However, when I try to use some basic logic every time the passed variable is updated, all the items inside the "animateViews" function apparently turn nil. I receive the infamous "unexpectedly found nil" error. This only happens when i try to use the variable I'm passing, everything runs as expected otherwise outside of the function.
import UIKit
class MessagesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var messageTableView: UITableView!
#IBOutlet weak var boostCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
...all cells, delegates, and datasource are registered and setup in here.
}
func animateViews(selectedIndex: Int) {
if selectedIndex == 0 {
print("LEFT") // prints as expected
self.messagesTableView.isHidden = false // unexpected found nil error on each of these items.
self.boostCollectionView.isHidden = true
} else if selectedIndex == 1 {
print("RIGHT") // prints correctly when updated
self.messageTableView.isHidden = true
self.boostCollectionView.isHidden = false
}
}
}
I've been playing around with this for far too long, how do I get views in the animateViews function to hide and unhide using the variable I'm passing from the segmented Control?
There is probably an amazingly simple fix that I'm not getting here, but I appreciate you for getting your eyeballs this far. Thank you!
This is happening because by the time you call
messagesViewController.animateViews(selectedIndex: selectedIndex), the MessagesViewController is not instantiated. That also means that the Table view and Collection View are not drawn yet and you are trying to use it before it is being created.
The viewDidLoad() method has to be called once to use the UI Elements of the View Controller.
Each time this line is called let messagesViewController: MessagesViewController = storyboard.instantiateViewControl, a new instance of the view controller is created.
Suggestion:
My suggestion will be to create an outlet of the segmented controls(seems like you are having multiple Segmented Controls in View Controller), and create a method like #IBAction func indexChanged(_ sender: AnyObject) { and assign this action to the value changed property of the segmented controls.
You can differentiate among the segmented controls using the sender parameter.
I hope someone can help me with this problem I am getting with swift.
I am trying to add an array of UIViewControllers to a UIPageViewController. However, whenever I try to access a view controller through the method instantiateViewController, I get a SIGABRT error.
let vc: UIViewController = self.storyboard?.instantiateViewController(withIdentifier: views[index]) as! UIPageViewController
Here is my entire ViewController.swift file just for the reference.
import UIKit
class ViewController: UIViewController {
var pageViewController: UIPageViewController!
var views: [String] = ["view1","view2"]
override func viewDidLoad() {
super.viewDidLoad()
self.pageViewController = self.storyboard?.instantiateViewController(withIdentifier: "pageViewController") as! UIPageViewController
var arr: [UIViewController] = []
for i in 0..<views.count{
arr.append(assignView(index: i))
}
self.pageViewController.setViewControllers(arr, direction: .forward, animated: true, completion: nil )
self.addChildViewController(self.pageViewController)
self.view.addSubview(self.pageViewController.view)
pageViewController.didMove(toParentViewController: self)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func assignView (index: Int) -> UIViewController{
let vc: UIViewController = self.storyboard?.instantiateViewController(withIdentifier: views[index]) as! UIPageViewController
return vc
}
}
Can someone tell me why my code is throwing me this error?
Thank you so much!
From the line below:
self.storyboard?.instantiateViewController(withIdentifier: "pageViewController") as! UIPageViewController
It seems that storyboard is treated as optional. It may be that the storyboard variable is getting nil when you are trying to access it.Ideally it should not be nil and should not be optional.
If the optional is purposefully done then you can apply a "if let" to check before you access.
Let me know if you need further clarification.
SIGABRT is a controlled crash and the app terminated mostly on purpose. It mostly resulted from programmer's fault.
Look for following as there is two possible causes to crash.
self.storyboard doesn't properly got initialised as there is no corresponding storyboard or incorrectly referred.
View controller's identifier is incorrectly referred.
Consider the following example
var mainView: UIStoryboard!
mainView = UIStoryboard(name: "Main", bundle: nil)
let samplecontroller : UIViewController = mainView.instantiateViewController(withIdentifier: "iPhone")
In the above example mainView is my instance referring to StoryBoard which is implicitly unwrapped(this is done on purpose to specify that this cannot be nil)
samplecontroller is the instance of the scene in the storyBoard. "iPhone" is the name of the StoryBoardID of the scene.
This snippet will ensure that the mainView will never be nil provided my storyboard is setup correctly with the Ids and names used.
After this you can still have a safety check like
if let test = mainView {
//Do your stuff
}
Let me know if this helps.