Swift5 Data is Nil when passed, even when it isn't - ios

I have a strange problem in Swift.
It is related to other questions on passing data between view controllers, however the difference here is that this is intended to pass a simple object loaded from a 'didSelectRowAt' method.
I am passing data from a table view controller when the row is selected. The data is passed as an object and I can print the data and see it in the council BUT when I try to assign values from it, I get the dreaded: 'Unexpectedly found nil while implicitly unwrapping an Optional value' error. Not sure what I am missing here!
From Table View Controller A:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "existingStudentSegue", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! StudentDetailTableViewController
if let indexPath = tableView.indexPathForSelectedRow {
destinationVC.selectedStudent = studentsArray?[indexPath.row]
}
}
Received by Table View Controller B:
var selectedStudent: Students? {
didSet {
print("DATA RECEIVED: \(String(describing: selectedStudent))")
print("DATA TYPE: \ (type(of: selectedStudent))")
firstNameText.text = selectedStudent?.firstName
}
}
The 'print' statement shows the test data I input and the 'type of' displays the Students class I setup as follows.
DATA RECEIVED: Optional(Students {created = 2019-09-24 12:07:23 +0000; name = Anna Altitude; ...(it's all here)
DATA TYPE: Optional<Students>
The code fails when I attempt to display the text in a text field in Table View Controller B. I see in the call stack, the data for selectedStudent is blank. But how can that be? The object is populated and accessible.

Add this inside viewDidLoad
firstNameText.text = selectedStudent?.firstName
The problem is when didSet triggers the firstNameText is nil as the vc isn't yet loaded

Related

How to refresh container view (reload table view) after getting data from firebase: Swift

I have been working on an app. I am a beginner so please ignore any mistakes.
The problem is that i have a view controller, which has 2 container view controllers controlled by a segmented control.
enter image description here
All three of them have separate classes: (say)
MainViewController
FirstViewController
SecondViewController
In the main view controller, i am getting some data from firebase, which i am storing in an array, and this array is to be passed to the first and second container views, which have their table views, which will load data based on this array which is passed.
Now before the data comes back in the MainViewController, the First and Second view controllers are already passed with an empty array, and no data loads up in their table views (obviously because the array is empty).
I want the container view controllers to load up after the data is received, and array is loaded. Any help ?, Thanks
P.s I am not performing any segue because these are container views, and they are automatically loaded as the main view container loads.
EDIT: Being more precise and clear with original code:
Originally I have 3 view controllers
SearchResultsScreenViewController (Main VC)
GuidesListSearchScreenViewController (First Container VC)
ServicesListSearchScreenViewController (Second Container VC)
In the Main VC i used a segmented control to see container vc's on screen, here:
import UIKit
import Firebase
class SearchResultsScreenViewController: UIViewController
{
#IBOutlet weak var GuideListView: UIView!
#IBOutlet weak var ServicesListView: UIView!
var searchQueryKeyword: String?
var guidesDataArray = [GuideDM]()
override func viewDidLoad()
{
super.viewDidLoad()
ServicesListView.isHidden = true
populateGuidesList()
}
#IBAction func SegmentChanged(_ sender: UISegmentedControl)
{
switch sender.selectedSegmentIndex
{
case 0:
GuideListView.isHidden = false
ServicesListView.isHidden = true
break
case 1:
GuideListView.isHidden = true
ServicesListView.isHidden = false
break
default:
break
}
}
func populateGuidesList()
{
let dbRef = Firestore.firestore().collection("guide")
dbRef.getDocuments
{ (snapshot, error) in
if let err = error
{
print(err.localizedDescription)
print("Error: Unable to find guides list")
}
else
{
if let snap = snapshot
{
print("List is started now")
for doc in snap.documents
{
if doc.exists
{
let data = doc.data()
let city = data["city"] as? String ?? ""
let province = data["province"] as? String ?? ""
let country = data["country"] as? String ?? ""
if city.localizedCaseInsensitiveContains(self.searchQueryKeyword!) || province.localizedCaseInsensitiveContains(self.searchQueryKeyword!) || country.localizedCaseInsensitiveContains(self.searchQueryKeyword!)
{
let guideId = doc.documentID
let guideEmail = data["email"] as? String ?? ""
let name = data["name"] as? String ?? ""
let dob = data["dob"] as? String ?? ""
let feeCurrency = data["feeCurrency"] as? String ?? ""
let status = data["status"] as? String ?? ""
let totalReviews = data["totalReviews"] as? Int ?? 0
let rating = data["rating"] as? Int ?? 0
let baseFee = data["baseFee"] as? Int ?? 0
let isGuideFeatured = data["isGuideFeatured"] as? Bool ?? false
//make a model of guide and append in array
let guide = GuideDM(id: guideId, email: guideEmail, name: name, dob: dob, city: city, province: province, country: country, feeCurrency: feeCurrency, status: status, baseFee: baseFee, rating: rating, totalReviews: totalReviews, isGuideFeatured: isGuideFeatured)
self.guidesDataArray.append(guide)
}
}
}
print("list is finalized now")
}
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.identifier == "searchScreentoGuideListSegment"
{
let guidesListContainerVC = segue.destination as! GuidesListSearchScreenViewController
guidesListContainerVC.guidesDataArray = self.guidesDataArray
}
}
}
In the above class my code makes a call to function "populateGuidesList()" which makes a network call to get data, and at the same time loads up my container views. The problem is, before the network call returns data, the empty array gets passed to my "GuidesListSearchScreenViewController" i.e. (First container VC), which is a table view, and loads an empty table because the array is not filled yet.
My First container VC class:
import UIKit
import Firebase
class GuidesListSearchScreenViewController: UIViewController
{
#IBOutlet weak var guidesListTableView: UITableView!
var guidesDataArray = [GuideDM]()
override func viewDidLoad()
{
super.viewDidLoad()
guidesListTableView.delegate = self
guidesListTableView.dataSource = self
guidesListTableView.register(UINib(nibName: "GuidesListCellSearchScreenTableViewCell", bundle: nil), forCellReuseIdentifier: "guidesListCell")
}
}
extension GuidesListSearchScreenViewController: UITableViewDataSource, UITableViewDelegate
{
// below functions are to setup the table view
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return guidesDataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = guidesListTableView.dequeueReusableCell(withIdentifier: "guidesListCell") as! GuidesListCellSearchScreenTableViewCell
//adding properties to cell and then returning cell
return cell
}
}
GOAL: Either load the container view, after the data is received in the array, or refresh the table by again passing the array to container VC and reloading table.
Other solution: I had tried loading up all this array data inside First container VC class, and reloading table view data from there, which works perfectly fine, but to me which is a very inefficient approach, as i need this array in both container views, so making network calls for each container vc seems very inefficient. Therefore, i am trying to get the data once and pass in both container views. Kindly correct me if you feel me wrong.
P.s I have deleted other functionality and simplified the code.
And help would be highly appreciated.
The container view controllers will load when the main view controller is loaded. Therefore you will have to update the container view controllers when the main view controller receives the data.
A simple way to do this is to update the arrays in the container view controllers, and use a didSet on the arrays to force the container view controllers to reload themselves.
For example, if your FirstViewController displays the array data in a table view, you might do this:
var array: [ArrayItem] {
didSet {
tableView.reloadData()
}
}
Then in your main view controller, when the data is received, set this property:
getData() { resultArray in
firstViewController.array = resultArray
}
Please note, since you didn't provide any code, these are just examples and you will have to adjust them to fit your specific situation.
EDIT: Per comment below, you should be careful not to set the array in your FirstViewController before its view has loaded or the call to tableView.reloadData() will cause your app to crash.
I can only speculate without seeing the code, so here goes nothing... 😉
Try doing the following on viewDidLoad() of MainViewController:
Use the property related to FirstViewController (for the sake of my explanation let’s assume it’s named ‘firstVC’). What you want to do is go, firstVC.view.isHidden = true
Do the same to SecondViewController.
Why? Doing so will hide the container VC’s (ViewController) from the MainViewController’s view.
Now what you want to do is, at the place where you get data from Firebase (at the closure), add the following:
firstVC.view.isHidden = false
Do the same to SecondViewController.
This brings it back to view with the data you fetched already populating it.
Hopefully this helps you out some way.

How can I transfer multiple rows of a tableView to another ViewController

I'm trying to add a feature in my app, to add multiple members to one action at one time. The members are listed in a tableView and the user can select multiple rows at one time with the .allowsMultipleSelection = true function. I got the following code but this doesn't work. I think my idea would work but not in the way I have it in the code :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? AddMultipleMemberTransactionViewController,
let selectedRows = multipleMemberTableView.indexPathsForSelectedRows else {
return
}
destination.members = members[selectedRows]
}
Does somebody out here know, how I can solve this problem, because there is an error :
Cannot subscript a value of type '[Member?]' with an index of type '[IndexPath]'
I have the same feature in the app but just for one member. There I in the let selectedRows line after the indexPathForSelectedRow a .row. Is there a similar function for indexPathsForSelectedRows ?
Or is this the wrong way to do it?
You need
destination.members = selectedRows.map{ members[$0.row] }
As the indexPathsForSelectedRows indicates, it returns an array of IndexPath. What you need to do is create an array of Member objects based on those path.
Assuming you have a "members" array that contain all the members the user can select from, and your table has only 1 section:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
var selectedMembers: [Member] = []
guard let destination = segue.destination as? AddMultipleMemberTransactionViewController,
let selectedIndexes = multipleMemberTableView.indexPathsForSelectedRows else {
return
}
for selectedIndex in selectedIndexes {
let selectedMember = members[selectedIndex.row]
selectedMembers.append(selectedMember)
}
destination.members = selectedMembers
}
You can also use the array map() function to change the for loop into a single line operation:
let selectedMembers: [Member] = selectedRows.map{ members[$0.row] }
Both should effectively do the same.

Cleaning UINavigationController after show in Swift

I have very strange bug, that I didn't have in objective-c.
I have two navigations controller one after another. In first I have UITableView. On cell click I navigate to second controller, and with clicking back button I navigate to first navigation controller. (I don't do anything else.) My memory go up every time that I navigate to second controller but it doesn't go down when I go back.
Code that I have :
First View Controller :
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "segue", sender: self)
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segue",
let destination = segue.destination as? SecondViewController,
let rowIndex = table.indexPathForSelectedRow {
let item = allItems[rowIndex.section][rowIndex.row]
destination.itemId = segue.id as Int?
destination.coreDataManager = self.coreDataManager
}
}
Second View Controller
override func viewDidLoad() {
super.viewDidLoad()
// reload data
reloadData()
}
private func reloadData() {
// We need database ready to reload data from server
guard coreDataManager != nil else {
print("We try to prepare view, but core data is not ready jet.")
// stop loader
self.loader.stopLoader()
return
}
self.model = Model.init(itemId: itemId! as NSNumber!, managadContext: coreDataManager!.privateChildManagedObjectContext())
}
Model object is object from Objective-c library.
I know that this object is problematic, because if I comment out last row, the step memory graph disappear.
I use same library with same call in previous Objective-C application and I didn't have this memory leak.
I try with :
deinit {
self.model = nil
}
but it didn't help.
How to solve this memory leak, because if you look at the graph it is quite huge. After opening 4 cells I have 187 MB memory used.
EDIT:
I figured that deinit {} is never called.
SUGGESTION
I make coreDataManager as weak var:
weak var coreDataManager: CoreDataManager? // weak property of Second Controller
FULL CODE:
Second controller
import UIKit
import BIModel
class SecondViewController: UIViewController {
// MARK: - Properties
/// This object represent connection to database.
weak var coreDataManager: CoreDataManager?
/// This is a Server object that represent on witch server we try to connect.
var currentServer: Server?
/// This is a cube id that we would like to open in this dashboard.
var itemId: Int? = 1193 // TODO: replace this
/// This is a model object. This object holds all calculations, all data,...
var model : Model?
// MARK: - Life Cicle
override func viewDidLoad() {
super.viewDidLoad()
// reload data
reloadData()
}
private func reloadData() {
// We need database ready to reload data from server
guard coreDataManager != nil else {
print("We try to prepare view, but core data is not ready jet.")
return
}
guard itemId != nil else {
return
}
self.model = Model.init(cubeId: currentCubeId! as NSNumber!, managadContext: coreDataManager!.privateChildManagedObjectContext())
}
einit {
print("DEINIT HAPPENED")
model = nil
}
}
I clean code a little bit. This is now whole code. "DEINIT HAPPENED" is printed, but memory stack is the same.

String not being passed to next view controller from prepareForSegue

I have a push segue on my StoryBoard which is named toGuestVC.
I use that to segue to the next ViewController in my didSelectRowAtIndexPath method like so:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let username = followUsernameArray[indexPath.row]
performSegue(withIdentifier: SG_TO_GUEST_VIEW_CONTROLLER, sender: username)
}
Then in my prepareForSegue:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == SG_TO_GUEST_VIEW_CONTROLLER {
if let nextVC = segue.destination as? GuestCollectionVC, let sender = sender as? String {
print("PRINTING NEXT VC: \(nextVC)") //This prints out the memory address. Not sure if this is what you meant by print nextVC.
nextVC.guestUser = sender
}
}
}
For some reason this line in my prepareForSegue is not running:
nextVC.guestUser = sender.username
When I try to print out the value guestUser in my nextViewController the value of guestUser is nil. But when I print out the value of sender in my prepareForSegue method it is not nil.
So is my sender value not being passed to the next ViewController? I can't find a solution to this problem any ideas?
GuestCollectionVC Implementation:
import UIKit
import Parse
private let reuseIdentifier = "Cell"
class GuestCollectionVC: UICollectionViewController {
var guestUser: String!
override func viewDidLoad() {
super.viewDidLoad()
print("PRINTING SELF IN GuestCollectionVC: \(self)")
loadPosts()
}
func loadPosts() {
//Load posts query
let query = PFQuery(className: PF_POSTS_CLASS)
query.limit = postCount
//Getting error here below this comment when I use guestUser since it is nil
query.whereKey(PF_POSTS_USERNAME_COLUMN, equalTo: guestUser)
query.findObjectsInBackground { (result: [PFObject]?, error: Error?) -> Void in
if error == nil {
if let result = result {
self.uuidOfPosts.removeAll(keepingCapacity: false)
self.imageArrayOfPFFIle.removeAll(keepingCapacity: false)
for postObject in result {
if let uuid = postObject[PF_POSTS_UUID_COLUMN] as? String, let pic = postObject[PF_POSTS_PIC_COLUMN] as? PFFile {
self.uuidOfPosts.append(uuid)
self.imageArrayOfPFFIle.append(pic)
}
}
self.collectionView?.reloadData()
}
}else if error != nil {
print("ERROR FROM GUEST COLLECTION VC FROM loadPosts FUNCTION: \(error?.localizedDescription)")
}
}
}
}
So this is my implementation in the GuestViewController. In my loadPosts method where I used the variable guestUser I am getting the error:
fatal error: unexpectedly found nil while unwrapping an Optional value
From console printing
PRINTING NEXT VC: "InstagramClone.GuestCollectionVC: 0x7a6c5cc0"
PRINTING SELF IN GuestCollectionVC: "InstagramClone.GuestCollectionVC: 0x7a6c5100"
it's now obvious that hidden unexpected instance of GuestCollectionVC was created. So, there are different errors occurs depending on order of this two objects invoke their viewDidLoad method (can be any order). Also there are can be other errors in nextVC viewDidLoad method, but this is other story for other question.
You got this problems because you created action segue, that works automatically on cell click (hidden view controller created), and at the same time you are perform this segue in code, creating second controller nextVC.
To solve issue, you should find and remove that segue and add new one, not action segue from some element of your view controller, but "empty" segue between view controllers. To create segue of this type you should select first view controller, hold "control" key and start dragging to next view controller from yellow square on top of first view controller (that symbol controller itself), choose "show", and set identifier.
Since I don't have the full context of the values within your tableView method I can only speculate. That said, the sender you're passing in should be the view controller:
performSegue(withIdentifier: "toGuestVC", sender: username)
You're passing in a value called username which looks to be a string value? It should be something like:
performSegue(withIdentifier: "toGuestVC", sender: self)
where self is your view controller. If you're passing in a string value to sender then in your prepareForSegue method then sender does not have a property called username sender actually is username. Therefore you should pass the value elsewhere:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let indexPath = self.tableView.indexPathForSelectedRow
if let nextVC = segue.destination as? GuestCollectionVC {
nextVC.guestUser = followUsernameArray[indexPath.row]
}
}

Passing NSManagedObject from one view controller to another

I have a uitableview which is filled with core data objects. I need to pass object for selected row to detail view controller. Following is my code for it:
Alert Screen:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowAlertDetails" {
if let destination = segue.destinationViewController as? AlertDetailsViewController {
if let blogIndex = tblvwAlerts!.indexPathForSelectedRow()?.row {
let objAlert:Alert = arrReferrals[blogIndex] as! Alert
destination.objAlert = objAlert
}
}
}
}
Detail View Controller:
class AlertDetailsViewController: UIViewController {
#IBOutlet weak var tblvwHitDetail: UITableView?
var objAlert:Alert = Alert()
I am getting following error when I am trying to copy object from first page to detail page:
CoreData: error: Failed to call designated initializer on NSManagedObject class 'Alert'
The error is caused by calling Alert() method in var objAlert:Alert = Alert(). You can set Alert as an Implicitly unwrapped value var objAlert:Alert!. It will be initially nil and then it will hold the reference of the managed objected which is passed from the previous controller.

Resources