Segue value nil in ViewDidLoad but has value in didSet - ios

I am passing a Core Data entity to the next View Controller with a prepareForSegue like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "MemberDetails" {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("MemberDetails") as! MemberDetails
let index = self.memberTable.indexPathForSelectedRow
if searchPredicate == nil {
let member = self.sections[index!.section].members[index!.row]
member.printMember()
vc.member = member
} else {
vc.member = self.filteredMembers[index!.row]
}
}
}
And in my receiving View Controller i have this:
var member : Member? {
didSet {
print("")
print(" --------------------- ")
print("")
member?.printMember()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
print("")
print(" --------View Did Load -------- ")
print("")
self.member?.printMember()
}
With the following output:
----------_-----------
// member.printMember() function output
--------View Did Load --------
// no ouput -> object is nil
This means that the didSet happens before the viewDidLoad and it has values but for some reason it is emptied again when the viewDidLoad is executed (object = nil)
Why is this happening? / How do i mitigate this effect?

I think you misunderstood what a segue is.
You are instantiating a new MemberDetails on prepareForSegue
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("MemberDetails") as! MemberDetails
If the prepareForSegue method is called, this means a viewController from the storyboard is already being loaded
what you need is
if segue.identifier == "MemberDetails" {
if let vc = segue.destinationViewController as? MemberDetails{
let index = self.memberTable.indexPathForSelectedRow
if searchPredicate == nil {
let member = self.sections[index!.section].members[index!.row]
member.printMember()
vc.member = member
} else {
vc.member = self.filteredMembers[index!.row]
}
}
}

Related

How to refresh DetailViewController when MasterViewController is updated

I have an application where the users can update and add tasks. But when the users update the task I need to update the details in the DetailViewController
When the users updates the task and saves the data in MasterViewController saves but the data in DetailViewController stays the same
How can I fix it so that the details get updated as well
The tasks are added and edited using the AddProjectController
I tried the following methods but none if them worked, all caused the the application to crash
let vc = DetailViewController()
let row = vc.view.layoutIfNeeded();
//Second method
let vc = MasterViewController()
vc.performSegue(withIdentifier: "showDetail", sender: vc.self)
The data is set in the DetailViewController using
override func viewDidLoad() {
super.viewDidLoad()
table.delegate = self
table.dataSource = self
lblName.text = detail.name ?? ""
lblCode.text = detail.code ?? ""
}
MasterViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object = fetchedResultsController.object(at: indexPath)
let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController
controller.details = object;
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
detailViewController = controller
}
}
}
EditViewController
func editEvents(_ sender: Any){
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Proj")
do {
let test = try context.fetch(fetchRequest)
var objectUpdate = test[0] as! Assesment;
objectUpdate.name = txtname.text ?? ""
objectUpdate.code = txtCode.text ?? "";
do {
try context.save()
}
catch {
print(error)
}
}
catch {
print(error)
}
}
I want to update the details when the editEvent function is finished executing
Is there anyway I can get this to work?
If You are using coreData .. You can implement FRC in details .. so you get refreshed data every time in callback ...
In your MasterViewController define a variable
Here is another workaround
private var controller : DetailViewController? = nil
Then in your Segue method use this controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let indexPath = tableView.indexPathForSelectedRow {
let object = fetchedResultsController.object(at: indexPath)
self.controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController // here you set your instence variable controller
controller.details = object;
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
controller.navigationItem.leftItemsSupplementBackButton = true
detailViewController = controller
}
}
}
func editEvents(_ sender: Any){
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Proj")
do {
let test = try context.fetch(fetchRequest)
var objectUpdate = test[0] as! Assesment;
objectUpdate.name = txtname.text ?? ""
objectUpdate.code = txtCode.text ?? "";
do {
try context.save()
if let detailsCotrollerExists = controller {
detailsCotrollerExists.updateDataWithObject(objectUpdate) // function you will write in detail class which takes object and refresh data there .. Either outlets or Table/Collection View reload
}
}
catch {
print(error)
}
}
catch {
print(error)
}
}
Then you write a function in DetailViewController func updateDataWithObject(_ obj: Assesment) function you will write in detail class which takes object and refresh data there .. Either outlets or Table/Collection View reload
Write function in DetailViewController
func updateDataWithObject(_ obj: Assesment) {
// Refresh data
}

Dynamically assign ViewController to Navigate

In my case I have UITableView and have View all button for the listing of all the items in separate screens. So I added target for UIButton action method in cellForRowAt. Now what I am doing in action method:
#IBAction func btnViewAllOffer(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tblOfferView)
let indexPath = self.tblOfferView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
if let type = self.homeData[indexPath!.section].type {
if type == HomeDataType.SponserProduct.rawValue {
let vc1 = self.storyboard?.instantiateViewController(withIdentifier: "ViewController1") as! ViewController1
if let title = self.homeData[indexPath!.section].title {
vc1.title = title
}
self.navigationController?.pushViewController(vc1, animated: true)
} else if type == HomeDataType.Offer.rawValue {
let vc2 = self.storyboard?.instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
if let title = self.homeData[indexPath!.section].title {
vc2.title = title
}
self.navigationController?.pushViewController(vc2, animated: true)
} else if type == HomeDataType.BestSeller.rawValue {
let vc3 = self.storyboard?.instantiateViewController(withIdentifier: "ViewController3") as! ViewController3
if let title = self.homeData[indexPath!.section].title {
vc3.title = title
}
self.navigationController?.pushViewController(vc3, animated: true)
}
}
}
}
What I need, is there any way I can minimize the code and assign viewcontrollers dynamically so there is no need to instantiate each view controller and push them everytime?
Something like:
var vc = UIViewController()
if let type = self.homeData[indexPath!.section].type {
if type == HomeDataType.SponserProduct.rawValue {
vc = ViewController1()
}
else if type == HomeDataType.Offer.rawValue {
vc = ViewController2()
} else if type == HomeDataType.BestSeller.rawValue {
vc = ViewController3()
}
}
self.navigationController?.pushViewController(vc, animated: true)
Use a protocol (SimilarViewController) to define the common properties like title:
protocol SimilarViewController {
var title: String? { get set }
}
class ViewController1: UIViewController, SimilarViewController {
var title: String?
}
class ViewController2: UIViewController, SimilarViewController {
var title: String?
}
class ViewController3: UIViewController, SimilarViewController {
var title: String?
}
#IBAction func btnViewAllOffer(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tblOfferView)
let indexPath = self.tblOfferView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
if let type = self.homeData[indexPath!.section].type {
var vcGeneric: SimilarViewController?
if type == HomeDataType.SponserProduct.rawValue {
vcGeneric = self.storyboard?.instantiateViewController(withIdentifier: "ViewController1") as! ViewController1
} else if type == HomeDataType.Offer.rawValue {
vcGeneric = self.storyboard?.instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
} else if type == HomeDataType.BestSeller.rawValue {
vcGeneric = self.storyboard?.instantiateViewController(withIdentifier: "ViewController3") as! ViewController3
}
if let title = self.homeData[indexPath!.section].title {
vcGeneric?.title = title
}
if let vcGeneric = vcGeneric as? UIViewController {
self.navigationController?.pushViewController(vcGeneric, animated: true)
}
}
}
}
1: create a struct and assign the value to it.
struct TitleDetails {
static var title : String = ""
}
2: create an extension of viewController and use it to avoid code repetition.
extension UIViewController {
func pushVC(_ vcName : String) {
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: vcname)
self.navigationController?.pushViewController(vc, animated: true)
}
}
3: now you can call it directly as,
TitleDetails.title = yourTitleValue
self.pushVC("ViewController1")
and in your ViewDidLoad() method of your destination view controller,
self.title = TitleDetails.title
Create BaseViewController and derived other ViewController from BaseViewController
class BaseViewController: UIViewController {
var viewTitle = ""
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func pushVC(_ vcName : String) {
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(vc, animated: true)
}
}
And use below code on ViewController you need like:
#IBAction func btnViewAllOffer(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tblOfferView)
let indexPath = self.tblOfferView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
if let type = self.homeData[indexPath!.section].type {
self.viewTitle = self.homeData[indexPath!.section].title
if type == HomeDataType.SponserProduct.rawValue {
self.pushVC("ViewController1")
} else if type == HomeDataType.Offer.rawValue {
self.pushVC("ViewController2")
} else if type == HomeDataType.BestSeller.rawValue {
self.pushVC("ViewController3")
}
}
}
}

Swift prepare for segue and dismiss controller

I am trying to dismiss a tableview controller with a navigation controller embeded after performing a segue.
Here's my code:
class SelectAlbum: UITableViewController {
..............
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = (segue.destination as? CustomNavigationBar)?.topViewController as? AddPhotoPostVC
else { fatalError("unexpected view controller for segue") }
guard let cell = sender as? AlbumListCells else { fatalError("unexpected cell for segue") }
switch SegueIdentifier(rawValue: segue.identifier!)! {
case .showAllPhotos:
destination.fetchResult = allPhotos
destination.headerTitleBtnString = cell.allPhotoTitle.text!
case .showCollection:
// get the asset collection for the selected row
let indexPath = tableView.indexPath(for: cell)!
let collection: PHCollection
switch Section(rawValue: indexPath.section)! {
case .smartAlbums:
collection = smartAlbums.object(at: indexPath.row)
case .userCollections:
collection = userCollections.object(at: indexPath.row)
default: return // not reached; all photos section already handled by other segue
}
// configure the view controller with the asset collection
guard let assetCollection = collection as? PHAssetCollection
else { fatalError("expected asset collection") }
destination.fetchResult = PHAsset.fetchAssets(in: assetCollection, options: nil)
destination.assetCollection = assetCollection
destination.headerTitleBtnString = cell.collectionTitle.text!
}
}
........
}
The issue is that if I dismiss the controller in the prepare for segue function than the segue is not performed and I get and error in my console log: "Attempt to present....whose view is not in the window hierarchy!".
How can I dismiss the controller and pass the data?
Thank you.

passing data with performSegueWithIdentifier nil

I have the segue setup as:
and the tableView row selection:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
// Ensure controller knows which dataset to pull from,
// so detail view is correct
var friendChat: Friend!
if searchController.active && searchController.searchBar.text != "" {
friendChat = filterMappedFriends[indexPath.row]
} else {
friendChat = mappedFriends[indexPath.row]
}
// Now set the conditional cases: if a friend then chat, if user then friend request if not user then can invite them:
if(friendChat.statusSort == 2) {
self.performSegueWithIdentifier("showIndividualChat", sender: friendChat)
} else if (friendChat.statusSort == 1) {
print("Can invite to be friend")
} else if (friendChat.statusSort == 0) {
print("Invite to Feast")
}
}
and the prepareForSegue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let indexPath = tableView.indexPathForSelectedRow {
// Ensure controller knows which dataset to pull from,
// so detail view is correct
let friendChat: Friend
if searchController.active && searchController.searchBar.text != "" {
friendChat = filterMappedFriends[indexPath.row]
} else {
friendChat = mappedFriends[indexPath.row]
}
// Now set the conditional cases: if a friend then chat, if user then friend request if not user then can invite them:
if segue.identifier == "showIndividualChat" {
let controller = segue.destinationViewController as! IndividualChatController
controller.friendChat = friendChat
controller.senderId = Global.sharedInstance.userID
controller.senderDisplayName = Global.sharedInstance.userName
}
}
}
However, the object friendChat, seen in controller.friendChat, of the destination controller is always nil.
How can I pass the data:
controller.friendChat = friendChat
controller.senderId = Global.sharedInstance.userID
controller.senderDisplayName = Global.sharedInstance.userName
to the destination controller successfully?
The first thing you are doing in didSelectRowAtIndexPath is deselecting the row, so when you try and access the selected row in prepareForSegue you are going to get no row selected.
Since you are passing the Friend instance as your sender to performSegueWithIdentifier you can just say let friendChat = sender as? Friend in prepareForSegue;
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
if segue.identifier == "showIndividualChat" {
if let friendChat = sender as? Friend {
let controller = segue.destinationViewController as! IndividualChatController
controller.friendChat = friendChat
controller.senderId = Global.sharedInstance.userID
controller.senderDisplayName = Global.sharedInstance.userName
}
}
}
For Swift 3
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
if segue.identifier == "showIndividualChat" {
if let friendChat = sender as? Friend {
let controller = segue.destination as! IndividualChatController
controller.friendChat = friendChat
controller.senderId = Global.sharedInstance.userID
controller.senderDisplayName = Global.sharedInstance.userName
}
}
}

Retrieving firebase data of a tableview cell to a view controller's label

I have like a social app with a sort of newsfeed. if u click on the users name from a post in the newsfeed, you will go to his profile. Now i can't retrieve the data from that specific cell/post to the other viewController.
so i have to display the user's profile, with he's username, etc. but that doesn't work?
i have a Post model:
class Post {
private var _postDescription: String!
private var _profileImageURL: String?
private var _likes: Int!
private var _username: String!
private var _postKey: String!
private var _timeStamp: String!
private var _postRef: Firebase!
var postDescription: String? {
return _postDescription
}
var likes: Int {
return _likes
}
var username: String {
return _username
}
var postKey: String {
return _postKey
}
var profileImageURL: String? {
return _profileImageURL
}
init(description: String, username: String, profileImageURL: String?) {
self._postDescription = description
self._username = username
self._profileImageURL = profileImageURL
}
init(postKey: String, dictionary: Dictionary<String, AnyObject>) {
self._postKey = postKey
if let likes = dictionary["likes"] as? Int {
self._likes = likes
}
if let desc = dictionary ["description"] as? String {
self._postDescription = desc
}
if let imgUrl = dictionary["profileImg"] as? String {
self._profileImageURL = imgUrl
}
if let user = dictionary ["username"] as? String {
self._username = user
} else {
self._username = ""
}
self._postRef = DataService.ds.REF_POST.childByAppendingPath(self._postKey)
}
}
this is my profileVC:
class ProfileVC: UIViewController {
#IBOutlet weak var username: UILabel!
var post: Post?
override func viewDidLoad() {
super.viewDidLoad()
username.text = post.username // gives me a nil error.
}
}
and i use a TapGestureRecognizer in my tableViewCell to perform the segue.
in my cellForRowAtIndexPath:
let profileLblTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(NewsVC.goToProfileScreen(_:)))
profileLblTapRecognizer.numberOfTapsRequired = 1
profileLblTapRecognizer.delegate = self
cell.usernameLabel.tag = indexPath.row
cell.usernameLabel.userInteractionEnabled = true
cell.usernameLabel.addGestureRecognizer(profileLblTapRecognizer)
and the goToProfileScreen function:
func goToProfileScreen(gesture: UITapGestureRecognizer) {
self.performSegueWithIdentifier("ProfileScreen", sender: self)
}
this is my datamodel on firebase:
UPDATE:
i tried this instead:
let profileLblTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(NewsVC.prepareForSegue(_:sender:)))
with this function:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ProfileScreen" {
if let cell = sender as? NewsCell, row = tableView.indexPathForCell(cell)?.row, vc = segue.destinationViewController as? ProfileVC {
vc.post = posts[row]
}
}
}
but that gave me an error on appDelegate: Thread 1: EXC_BAD_ACCESS(code=1, address = 0x1)
I've added this as an answer rather than a comment so that I can add and format some code examples.
When you call performSegueWithIdentifier, a NEW instance of the view controller identified by that segue is created, so all of its properties will be their defaults.
You have two ways of instantiating this view controller and setting properties before it loads. The first is the prepareForSegue option, in your case it may look something like this:
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if (segue.identifier == "ProfileScreen") {
let vc = segue.destinationViewController as! ProfileVC
vc.post = post
}
}
Another option is to create and present the view controller yourself, this example uses a storyboardID
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = mainStoryboard.instantiateViewControllerWithIdentifier("profileVC") as! ProfileVC
vc.post = post
presentViewController(vc, animated: false, completion: nil)
Update:
I'm not sure why you are adding a tap gesture recogniser to this, you could just use didSelectRowAtIndexPath, have a look at this other question and answer
you could have a property on your table view controller called selectedItem or something similar. and then in didSelectRowAtIndexPath set selectedItem to the item at the current index. Then in prepare for segue you would just do vc.post = selectedItem
Update Two:
After the op sharing their code privately, I noticed that the issue is that the user is using tapGestureRecogniser in the tableView. I added some code into the called function to get the row in which contained the tapped view, once I had the indexPath it was then easy to store it in a temporary property and retreive later in the prepareForSegue method, details below
// temp property
var selectedPost:Post?
// function called on tap
func viewProfile(sender:UITapGestureRecognizer) {
if (sender.state == UIGestureRecognizerState.Ended) {
let point = sender.locationInView(self.tableView)
if let indexPath = self.tableView.indexPathForRowAtPoint(point) {
selectedPost = self.posts[indexPath.row]
performSegueWithIdentifier("ProfileScreen", sender: self)
}
}
}
// Prepare for segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "ProfileScreen") {
let vc = segue.destinationViewController as! ProfileVC
if let post = selectedPost {
vc.post = post
}
}
}

Resources