I'm building an app (in XCode 8.2.1) where some objects are displayed on a 2D board, and when the user taps one of these objects some info should be displayed about it as a styled modal info box. My design is to have the info written in a separate view controller, which I would display when needed.
I've designed a basic stub for the second view controller and added a single label to it in the interface builder. Then I've ctrl-linked this label to my custom VC class:
class InfoViewController: UIViewController {
#IBOutlet weak var info: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
func displayInfo() {
info.attributedText = NSAttributedString(string: "abc")
}
}
However, when I test my app and tap the object, the info field is nil even in the viewDidLoad() method of my custom VC class. The way I'm displaying my VC is as follows:
let infoViewController = InfoViewController()
infoViewController.modalPresentationStyle = .overCurrentContext
self.present(infoViewController, animated: true, completion: nil)
infoViewController.displayInfo()
(Note: In the end I will have only one single instance of InfoViewController but this is just for testing. I don't expect having a global instance would make any difference?)
As I said, be it inside the viewDidLoad() method or in the displayInfo() method, info is always nil, such that setting its attributedString attribute crashes the app. Thinking the present method might be called asynchronously, I've tried calling displayInfo() from inside viewDidLoad(), but that didn't make any difference.
Can anyone tell my what I've forgotten that would allow my IBOutlet from being properly initialized properly?
Thanks!
David
The problem is the reference to InfoViewController(), which instantiates the view controller independent of any storyboard scene. You want to use instantiateViewController:
let infoViewController = storyboard?.instantiateViewController(withIdentifier: "Info") as! InfoViewController
infoViewController.modalPresentationStyle = .overCurrentContext
present(infoViewController, animated: true) {
infoViewController.displayInfo()
}
A couple of notes:
This assumes that (a) you've given the scene in the storyboard a "storyboard id"; (b) you've set the base class for that scene to InfoViewController.
Note, I called displayInfo in the completion handler of present because you probably don't want that called until the scene has been presented and the outlets have been hooked up.
Alternatively, you can update non-outlet properties of the InfoViewController immediately after instantiating it and then have its viewDidLoad take those properties and update the outlets, e.g.:
class InfoViewController: UIViewController {
var info: String!
#IBOutlet weak var infoLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
infoLabel.attributedText = NSAttributedString(string: info)
}
}
Note, I changed the #IBOutlet name to be infoLabel and added the String property called info. That tends to be the convention, that outlets bear some suffix indicating the type of control, and model objects, like the String property, are without the suffix. (You'll just want to make sure you remove that old outlet in the connections inspector in IB so that you don't have problems with these property name changes.)
Anyway, you can then do:
let infoViewController = storyboard?.instantiateViewController(withIdentifier: "Info") as! InfoViewController
infoViewController.info = "abc"
infoViewController.modalPresentationStyle = .overCurrentContext
present(infoViewController, animated: true, completion: nil)
The key point is don't try to update outlets of the scene immediately after instantiating it, but make sure that this is deferred until after viewDidLoad was called.
I Replaced
let vc = CCDetailViewController()
With
let vc = self.storyboard?.instantiateViewController(withIdentifier: "CCDetailViewController")
Finally
self.present(vc!, animated: true, completion: nil)
Now It Works...
In my case, I had created new view controller for the same class. Which had ended up with two view controllers in storyboard, but referring to the same class. After deleting old view controller, everything worked fine.
Hope it helps to someone.
Related
I have a UIViewController that has two containers and each Container is related to a UIViewController for some specific functionalities.
For people who are devaluating my question, it will be more helpful and appreciated if you put me on the right path instead.
what I am trying to do is pass data from the parent ViewController to the childViewControllers
I tried it using the protocol/delegate: But the problem is, I
couldn't assign the delegate to the childViewContainer since it
doesn't have an instance from the parent.
My second try was using the prepare function, but it doesn't work as well since the two containers load once the parent loads at first. so if the value is changed in the parentViewController I can't pass it again to the child.
Any Idea, please?
After a deeper digging I was able to find a solution for my own question.
here I am going to post if anyone else needs it in the future
so, first of all, I need it to lunch the ChildContoller from the parent controller and not from the storyboard ( so I deleted the segue between the parent and the child.
create a variable for childController like that:
lazy var firstChildViewController: FirstChildViewController = {
let storynoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storynoard.instantiateViewController(identifier: "firstChild") as! FirstChildViewController
self.addChild(viewController)
self.view.addSubview(viewController.view)
return viewController
}()
same thing for the other one if you have two children
and then in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
firstChildViewController.view.isHidden = false
secondChildViewController.view.isHidden = true
}
and then in the FirstChildViewController:
override func viewDidLoad() {
super.viewDidLoad()
if let parent = self.parent as? ParentViewController {
parent.delegate = self
}
}
And the problem is solved
Hope it helps someone
So all my controller's are done programmatically to avoid segues and that sort of complicated stuff.
I have a viewcontroller (Call it ProfileViewController) that downloads data from the network.
So I have a method in ProfileViewController that instantiates a single storyboard file with a static tableview with cells that have textfields in them. Here is the method:
ProfileViewController:
func userSelectedUpdateProfile() {
// Obtain reference to the only storyboard file named EditProfileSB
let storyboard = UIStoryboard(name: "EditProfileSB", bundle: nil)
// Since the Tableview is embedded in a navigation controller (with ID set to "navigationID")
if let parentNavigationController = storyboard.instantiateViewController(withIdentifier: "navigationID") as? UINavigationController {
// Now find the embedded TableViewController and access it's properties to pass to.
if let childEditController = parentNavigationController.topViewController as? EditProfileTableViewController {
// ! Error here ! Found nil when doing this.
childEditController.nameTextfield.text = "Passed this to static cell"
}
present(parentNavigationController, animated: true, completion: nil)
}
}
So the code itself is self-explanatory to what I am trying to achieve here. The TableView is embedded in a Navigation (done on storyboard with "Editor > Embed In") so on the 2nd nested if let statement I am now checking to find that Edit controller and access its properties (nameTextfield).
I get a crash when I attempt to access the nameTextField.text property. This textfield is set using storyboard. Here is that EditProfileTableViewController:
class EditProfileTableViewController: UITableViewController {
#IBOutlet weak var nameTextfield: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
// Other methods ...
}
Here is the storyboard flow layout.
Am I missing something here? I keep getting a crash on childEditController.nameTextfield.text = "Passed this to static cell" on the method userSelectedUpdateProfile().
If your View Controller still not call viewDidLoad().
your textfield is not create.
#IBOutlet weak var nameTextfield: UITextField!
you can see it's attribute is weak here.
Try create a value and pass text to the value. Then in viewDidLoad(), you can set the value to your textField
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 two classes:
class ExplorerViewController: UIViewController {
lazy var studyButton: ExploreButton = {
let button = ExploreButton()
button.setTitle("Study", forState: .Normal)
return button
}()
}
and
class ViewController: UIViewController, UISearchBarDelegate, LocateOnTheMap, GMSMapViewDelegate {
}
I'm trying to make it so that when I click the studyButton, it sends the button title to ViewController and goes to that view.
I'm not using storyboards and am having trouble with segues since every tutorial seems to give different examples that are specific to the things they've been working with and 95% of them seem to be operating with storyboard. Can someone give me a general way of how to do this?
How do I give the starting view controller an identifier because it isn't instantiated like the other controllers that I 'move' to after. How can I move from ViewController to ExplorerViewController and then move back to that same ViewController (with all changes intact).
Create an initializer for your ViewController that receives the "title" variable:
class ViewController: UIViewController, UISearchBarDelegate, LocateOnTheMap, GMSMapViewDelegate {
var btnTitle: String?
init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?, btnTitle:String?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
self.btnTitle = btnTitle
}
}
When creating the ViewController object use this initializer.
var viewController = ViewController(nibName: "ViewController", bundle: nil, btnTitle: title
You can initialize UIViewController that you want navigate to, assign data to properties in that controller and call this method:
presentViewController(viewController, animated: true, completion: nil)
For example:
let destinationViewController = ViewController()
destinationViewController.frame = self.view.frame
destinationViewController.buttonTitle = "title"
self.presentViewController(destinationViewController, animated: true, completion: nil)
Although I would suggest you to get familiar with Storyboards and perform navigation with Segues.
Make sure of two things:-
1.) You have given your viewController an StoryBoard ID lets say "viewControllerVC_ID" in it's Identity inspector
2.) You have NavigationController Embed in to your Initial entry point View Controller
In ViewController declare a variable
var btnLabelTxt : String!
Create an #IBAction of that button in ExplorerViewController :-
#IBAction func exploreBtnAction(sender : UIButton!){
let vcScene = self.navigationController?.storyboard?.instantiateViewControllerWithIdentifier("viewControllerVC_ID") as! ViewController
vcScene.btnLabelTxt = "Study"
//or you can just access the button itself in the viewController and set the title
//By vcScene.yourBtn.setTitle("Study", forState: .Normal)
self.navigationController?.pushViewController(vcScene, animated: true)
}
please see this question How to push viewcontroller ( view controller )? for how to switch between views.
to pass data once you have reference to the new view, you can assign the data to a property of that view.
My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.