Detail View Controller transition from Master View Contoller - swift

I am getting "unexpectedly found nil while unwrapping an Optional value" because in my code below I am trying to assign value to webview before its initialize. I am trying to transition from Master to Detail view controller.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
if let indexPath = self.tableView.indexPathForSelectedRow() {
let object = self.fetchedResultsController.objectAtIndexPath(indexPath) as NSManagedObject
let controller = (segue.destinationViewController as UINavigationController).topViewController as DetailViewController
controller.detailItem = object
controller.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem()
controller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
Detail View Code:
var detailItem: AnyObject? {
didSet {
// Update the view.
self.configureView()
}
}
func configureView() {
// Update the user interface for the detail item.
if let detailContent = detailItem?.valueForKey("content") as? String{
self.webView.loadHTMLString(detailContent as String, baseURL:nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.configureView()
}
It is failing because my Webview in Nil. How do I come around this situation where my outlets are not initialized while setting them.
Please help.
Thanks.

Stop and think about the order in which things happen:
prepareForSegue - The destination view controller exists, but that's all. It has no view and its outlets have not been set. You can set its non-outlet properties but that's all you can do.
The segue starts to happen.
The destination view controller gets viewDidLoad. Now it has a view and its outlets are set.
The segue completes and the destination view controller gets viewWillAppear: and later, viewDidAppear:. Now its view is actually in the interface.
So clearly you cannot permit configureView to assume that the web view exists, because the first time it is called, namely in prepareForSegue, it doesn't exist. configureView needs to test explicitly whether self.webView is nil, and if it is, it should do nothing:
func configureView() {
// Update the user interface for the detail item.
if self.webView == nil { return } // no web view, bail out
if let detailContent = detailItem?.valueForKey("content") as? String{
self.webView.loadHTMLString(detailContent as String, baseURL:nil)
}
}
After that, everything will be fine. viewDidLoad will subsequently be called, and configureView will be called again - and this time, both detailItem and the web view exist, so all will be well.

Related

Why Does My Object Not Transfer To The Next VC?

I am trying to transfer my object from HockeyDetailVC to my FavouritesVC using a button but my object is nil when I reach my second VC FavouritesVC. Why is it like that when I set the variable in my firstVC with my func transferObj()?
HockeyDetailVC
var item: CurrentPlayers?
override func viewDidLoad() {
super.viewDidLoad()
gonnaLoadView()
tableV.bounces = false
tableV.alwaysBounceVertical = false
favButton.layer.cornerRadius = 10
print(item) *//prints my current players object*
}
func transferObj() {
let otherVC = FavouritesVC()
otherVC.currentFav = item
print(item). *//prints my current player object*
}
#IBAction func addToFav(_ sender: Any) {
transferObj()
print("Favourite button Pressed")
}
FavouritesVC
var currentFav: CurrentPlayers?
override func viewDidLoad() {
super.viewDidLoad()
if currentFav == nil {
//display nil
self.tableView.separatorStyle = UITableViewCell.SeparatorStyle.none
print(favArr) *//prints empty array*
print(currentFav) *//nil*
} else {
favArr.append(currentFav!)
print(favArr)
}
}
As #Martin stated, let otherVC = FavouritesVC() creates a new instance of the controller, but it is not the instance that you will eventually display. So you are effectively setting the currentFav of a random FavouritesVC that will never actually be displayed, while the one you eventually do navigate to has it's currentFav property still unset.
To set the appropriate FavouritesVC instance, you need to access it in one of several ways (depending on how you present it). If it is through a segue, then you can reference it in the prepare(for segue: sender:) method. (When you create a Cocoa Touch Class file, the below method template is pre-populated. As it states, reference the new view controller using segue.destination.)
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
Alternatively, if you create and present the new view controller programmatically with something like
// 1.
let otherVC = storyboard?.instantiateViewController(withIdentifier: "yourFavouritesVCIdentifier")
// 2.
// 3.
self.show(otherVC, sender: self)
you can insert your otherVC.currentFav = item at line // 2..

Is it possible to observe changes in presentingViewController?

Is there any equivalent in Swift to RACObserve(self, presentingViewController)?
Or any other why to imitate this behaviour?
My issue is that I want to be notified whenever a view controller is "hidden" by another view controller. In objc what I'd do is to check if self.presentingViewController is nil.
Note that in this scenario there's no knowledge of which view controller is presented, so it's impossible to notify from within its viewDidAppear/viewDidDisappear.
As I understand your question: you need to to know which view controller is presented now and you need notification inviewDidAppear/viewDidDisappear.
So we can get this in several way.
The simple way is:
Get information of which is the top ViewController right now.
2.Call this method in your viewDidAppear/viewDidDisappear
Like this :
Get Which is The Top ViewController
func getTopViewController() -> UIViewController? {
if var topVC = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topVC.presentedViewController {
topVC = presentedViewController
return topVC
}
return topVC
}
return nil
}
Call in viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if let top = getTopViewController() {
print("topView Controller name \(top.title)")
top.view.backgroundColor = .red
}
}
Hope it will help you !

Access the presenting view controller from the presented view controller?

I have a view controller (containing my menu) presented on top of another view controller (my application).
I would need to access the presenting view controller (below my menu) from the presented view controller (my menu), for example to access some variables or make the presenting view controller perform one of its segues.
However, I just can't figure out how to do it.
I'm aware of the "presentingViewController" and "presentedViewController" variables but I didn't manage to use them successfully.
Any Idea ?
Code (from the presented VC, which as a reference to the AppDelegate in which the window is referenced) :
if let presentingViewController = self.appDelegate.window?.rootViewController?.presentingViewController {
presentingViewController.performSegue(withIdentifier: "nameOfMySegue", sender: self)
}
Here is a use of the delegation Design pattern to talk back to the presenting view controller.
First Declare a protocol, that list out all the variables and methods a delegate is expected to respond to.
protocol SomeProtocol {
var someVariable : String {get set}
func doSomething()
}
Next : Make your presenting view controller conform to the protocol.
Set your presenting VC as the delegate
class MainVC: UIViewController, SomeProtocol {
var someVariable: String = ""
func doSomething() {
// Implementation of do
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Your code goes here.
if let destVC = segue.destination as? SubVC{
destVC.delegate = self
}
}
}
Finally, when you are ready to call a method on the presenting VC (Delegate).
class SubVC: UIViewController {
var delegate : SomeProtocol?
func whenSomeEventHappens() {
// For eg : When a menu item is selected
// Set some Variable
delegate?.someVariable = "Some Value"
// Call a method on the deleate
delegate?.doSomething()
}
}
Assuming that VCApplication is presenting VCMenu, in VCMenu you can access VCApplication with:
weak let vcApplication = self.presentingViewController as? VCApplicationType
Your example self.appDelegate.window?.rootViewController?.presentingViewController is looking for the ViewController that presented the rootViewController - it will be nil.
EDIT
Per TheAppMentor I've added weak so there are no retain cycles.

Delegate between Container View and ViewController in Swift

I asked that question before, and I got the solution to what he sought. Now, I need to amplify my question. Using delegates, how can I create a Delegate to the ViewController send data to the ContainerView and ContainerView send data to the ViewController?
Well, I don't know if this is entirely what you're looking for, but what I've always done in this situation is kept a record of each view controller inside the other's class.
For example if your container view has the embed segue with identifier "Embed Segue" then your classes might look like:
Superview Class
Class ViewControllerOne: UIViewController {
var data = "This is my data I may want to change"
var subView: ViewControllerTwo?
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "Embed Segue" {
let destinationVC = segue.destinationViewController as! ViewControllerTwo
destinationVC.superView = self
self.subView = destinationVC
}
}
}
Embedded Class
Class ViewControllerTwo: UIViewController {
var data = "This is the other view controller's copy of that data"
var superView: ViewControllerOne?
}
Then you can pass data between these View Controllers simply by referencing self.subView.data and self.superView.data respectively.
Edit: For ViewControllerTwo to pass data back to ViewControllerOne, it would then simply have to reference self.superView.data. e.g:
Class ViewControllerTwo: UIViewController {
var data = "This is the other view controller's copy of that data"
var superView: ViewControllerOne?
func passDataBack() {
self.superView.data = self.data
}
}
This would then update the data variable in the first view controller.

Passing Parse objectID created on prepareForSegue

I am trying to pass the Parse Class "Conversation" objectId after it's created to another View Controller. When I check the data does not pass to the variable I point to.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object(s) to the new view controller.
let selectedIndex = self.tableView.indexPathForSelectedRow()?.row
let destinationVC = segue.destinationViewController as ConversationViewController
//Save Conversation Data
var convo = PFObject(className: "Conversation")
convo.saveInBackgroundWithBlock{
(success: Bool!, error: NSError!) -> Void in
if (success != nil) {
//Save Selected Person
participant["conversationId"] = convo.objectId as String!
participant.saveInBackground()
}
else{
NSLog("%#", error)
}
}
//Trying to Pass convo.objectId
destinationVC.newConversationId = convo.objectId as String!
}
The problem with the code is that convo.objectId is not set at the point where you use it. It gets set in the completion block of saveInBackgroundWithBlock:, but that block runs after the code that appears underneath it.
So what to do? If the next vc needs the convo object to be saved before it runs, then the correct pattern is to run the segue after the save. Find the point in your code where you are initiating the segue and replace it with the convo.saveInBackgroundWithBlock. Then do the performSegue from within that block.
Edit - Here's how doing this looks in objective-C. In either language, in order to do this, you must initiate the segue from code. Say you have the segue painted from a table view cell in IB (or storyboard) to the next view controller. Delete that segue, and control-drag a new one starting from the view controller which contains the table. (Select the view controller in IB and drag from there. Then, using the attributes inspector, give that segue and identifier, say, "ConvoSegue").
// since a table selection that starts the action, implement the selection delegate method
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
// we decide here that the convo object must be saved, and
// a segue should happen to another vc that needs the convo object:
var convo = PFObject(className: "Conversation")
convo saveInBackgroundWithBlock:^(BOOL success, NSError *error) {
if (!error) {
// now that convo is saved, we can start the segue
[self performSegueWithIdentifier:#"ConvoSegue" sender:convo];
} else {
// don't segue, stay here and deal with the error
}
}];
}
Notice in the above, we passed convo as the sender of the segue. This allows access to it in prepareForSegue. You can skip that if convo is a property of this view controller.
Now prepare for segue looks like yours, except without the asynch save...
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
// we don't need the selected row from the table view, because we have
// the convo object as the sender
// let selectedIndex = self.tableView.indexPathForSelectedRow()?.row
let destinationVC = segue.destinationViewController as ConversationViewController
// sorry, back to objective-c here:
PFObject *convo = (PFObject)AnyObject; // need a cast to use properly in objective-c
// deleted your save logic. just pass convo's id
//Trying to Pass convo.objectId
destinationVC.newConversationId = convo.objectId as String!
}