I was reading the thread: IBOutlet of another view controller is nil
I have a problem very similar to that.
RequestViewController
class DenunciasResueltasViewController: UIViewController {
#IBOutlet var mapView: GMSMapView!
var solicitudes = [SolicitudesModel]()
var tempMap: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
let camera = GMSCameraPosition.camera(withLatitude: 3.4824182, longitude: -8.1776567, zoom: 15)
self.mapView.camera = camera
}
func recenterMap(latitude:Float!, longitude:Float!) -> Void {
let coordinates = CLLocationCoordinate2DMake(CLLocationDegrees(latitude), CLLocationDegrees(longitude))
mapView = tempMap
self.mapView.animate(toLocation: coordinates)
}
RequestTableViewController
class RequestTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Some code to fill the table
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "RequestVC") as! RequestViewController
viewController.recenterMap(latitude: solicitudes[indexPath.row].getLatitude(), longitude: solicitudes[indexPath.row].getLongitude() )
return cell
}
}
Both components are initialized at same time in runtime, I mean, both are part of the same View.
And when the users click in a 'row' I wanna update the mapView
for that resason, I'm using the method 'RecenterMap'
But, the variable 'self.mapView' is always 'nil'.
How I can update this value?
You should use the delegate method didSelectRowAtIndexPath to do the recentering, because this is called when the user tapps on a cell. It could be that the Outlets of the first viewcontroller haven't yet been set when the second starts to fill its cells.
This is a common problem, wanting to access properties of a currently offscreen vc before you are going to present it.
You can solve it by forcing the view hierarchy to be build by accessing its view:
let _ = viewController.view
After this you are free to access any view-related property on the viewcontroller. So you should put it before you call .recenterMap
It's not really clear what are you trying to do: your code seem to instantiate a view controller with map for each table cell, but you don't actually present it after being instantiated.
You instantiate the RequestViewController, but it doesn't appear in the view hierarchy, thus the IBOutlet properties are not instantiated at that moment when you call them. Usually calling a view on a UIViewController instantiates all the outlets on it.
Alternatively, you could call self.present(requestViewController) in order for it to be presented.
What i understood is that, you have both the RequestViewController and RequestTableViewController, both are subview of a parent View,
1- your center on map function needs to be called on the delegate method :
didSelectRowAtIndexPath:
2- You need a reference to the RequestViewController and DO NOT INSTANTIATE IT FROM THE STORYBOARD. as you will get a new instance and not the one that is displayed already.
Related
I'm working on an iOS app and there's one small bug. My project contains the following:
Custom searchbar on top of a mapView (HomeViewController.swift)
View with a tableview which displays the searchresults (LocationSearchTable.swift)
When searching I add the view with the tableview on top of my mapview and behind my searchbar. This all works fine.
But now when I select a searchresult from the tableView it crashes because it says that my mapView is nil.
I'm having the following in HomeViewController.swift:
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
mapView.delegate = self
}
Adding the LocationSearchTable to the mapView like this:
func didChangeSearchText(searchText: String) {
if (searchText == "" ) {
self.locationSearchTable.view.removeFromSuperview()
} else {
self.view.insertSubview(self.locationSearchTable.view, aboveSubview: mapView)
}
}
And on the LocationSearchTable.swift I want to call the mapView when tapping on something in the list:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let mapView = HomeViewController().mapView
print(mapView)
}
When trying to access the mapView from there it's nil. Why is that? And what am I missing here?
Kind regards,
Wouter
You creating a new instance of HomeViewController and then asking for its mapView before the method awakeFromNib has occurred. The mapView at that point will be nil until the HomeViewController controller is added to a screen
The best way to handle this if I'm reading your hierarchy right is to delegate back to the view with the map from you tableview.
Or you can pass a reference to mapView into The tableView
Full code for this branch here
View controller "MovieDetailsVC" is presented to the navigation controller when a cell is selected.
The presenting view controller, "ViewController", stores the row of the tableView to display in NSUserDefaults as an Int.
"MovieDetailsVC" reads the row ok. It then pulls the whole array of custom class info from CoreData and stores the array row in a property.
The data is displayed ok at first. The IBOutlet connections are setup ok. I've disconnected and reconnected twice all outlets on MovieDetailsVC, so that should be ok.
"viewDidLoad" is called a successive time. Not sure from where. When it is called, the coredata entity and row number are pulled ok.
The issue is at line "lblTitle.text = self.movieRecord.title". I'm assuming any of the IBOutlets would cause the same issue.
The error thrown is what you would see if the outlets were not connected:
fatal error: unexpectedly fond nil while unwrapping Optional value.
code for the MovieDetailsVC is below. Any ideas why this outlet link would break after working ok would be greatly appreciated.
import UIKit
import CoreData
class MovieDetailsVC: UIViewController {
#IBOutlet var lblTitle: UILabel!
#IBOutlet var lblDescr: UILabel!
#IBOutlet var lblLink: UILabel!
#IBOutlet var imgMovie: UIImageView!
var movieRecord:FavMovie!
var favMovies = [FavMovie]()
override func viewDidLoad() {
super.viewDidLoad()
fetchAndSetResult()
}
func fetchAndSetResult() {
let app = UIApplication.sharedApplication().delegate as! AppDelegate
let context = app.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "FavMovie")
do {
let results = try context.executeFetchRequest(fetchRequest)
self.favMovies = results as! [FavMovie]
} catch let err as NSError {
print(err.description)
}
if let row = NSUserDefaults.standardUserDefaults().objectForKey("movieRow") as? Int {
self.movieRecord = self.favMovies[row]
configureCellDescr()
}
}
func configureCellDescr() {
lblTitle.text = self.movieRecord.title
lblDescr.text = self.movieRecord.descrWhyGood
lblLink.text = self.movieRecord.linkImdb
imgMovie.image = self.movieRecord.getImg()
}
}
I just have a look at your source code in github and find the problem. There are two issues and I will explain it following.
it does that the second time that the app overrides viewdidload
The reason that your code would call the viewDidLoad method twice is because you have a segue in your storyboard that connect the tableViewCell to movieDetailVC. This will present the movieDetailVC once you click the cell.
And in your code for didSelectCell method, you create another movieDetailVC object and present it.
So actually movieDetailVC would be presented twice when you click the cell. This cause the issue.
Any ideas why this outlet link would break after working ok would be greatly appreciated
The reason why the IBOutlet is nil is because of the way you present movieDetailVC in your code. You create the movieDetailVC object using: let movieDetailsVC = MovieDetailsVC(). Doing it this way, the IBOutlets will not be connected correctly, because ios dont know about the storyboard information.
The correct way to create a MovieDetailVC object is to instantiate from storyboard. See this post for difference.
Solution
There is a very simple solution for your code design:
just remove let movieDetailsVC = MovieDetailsVC() and self.navigationController?.presentViewController(movieDetailsVC, animated: true, completion: nil) from your ViewController class. Since you save the selection data in NSUserDefault, the cell segue will present movieDetailVC and your movieDetailVC can also get the selection data from userDefault.
I have a UICollectionView with a CollectionReusableView header. I want to pass a string from the collecitonview to the header, so that the header knows which data to load based on the string. I am trying to use delegates/protocols to do this, but keep getting "unexpectedly found nil while unwrapping an optional value." Here is my code:
protocol UserToQuery {
func thisUser(x: String)
}
class Profile: UICollectionViewController {
var ownProfile = true
var delegate:UserToQuery?
override func viewDidLoad() {
super.viewDidLoad()
if self.ownProfile == true {
let username = PFUser.currentUser()?.username
self.delegate!.thisUser(username!)
}
}
}
And here is the code for the Header view:
class ProfileHeader: UICollectionReusableView, UserToQuery {
var id1 = String()
var controller = Profile()
override func awakeFromNib() {
print(id1)
controller.delegate? = self
}
func thisUser(x: String) {
self.id1 = x
getProfileInfo()
}
func getUserData() {
// code here uses the id1 value to get data
}
}
My understanding of delegates/protocols is this: if you want to pass data (i.e., string), to another view, you make the view that receives the string conform to a protocol. This protocol includes a function that is used to pass the string, and when that function is called, it notifies the other view that the string is now available for use, and then you code what you want and use the string. Is that accurate?
In ProfileHeader, you have a variable, controller, which is creating a new instance of Profile, which is NOT the Profile view controller from your storyboard. This is why self.delegate! is nil in Profile.viewDidLoad().
I am going to make the assumption that ProfileHeader is a view in the Profile view controller. In your viewDidLoad, you should set the delegate to the ProfileHeader. See the example code below (I assume an outlet for the ProfileHeader view):
EDIT: ProfileHeader is not an outlet, as mentioned in the comments. Updated my answer to reflect that.
class Profile: UICollectionViewController {
var ownProfile = true
var delegate:UserToQuery?
override func viewDidLoad() {
super.viewDidLoad()
// Set the delegate!
self.delegate = ProfileHeader()
if self.ownProfile == true {
let username = PFUser.currentUser()?.username
// Delegate won't be nil now
self.delegate!.thisUser(username!)
}
}
}
}
As a general flow, the view controller should keep references to the view, not the other way around. So remove the controller property from your ProfileHeader view. The view shouldn't care what view controller is controlling it.
You have some misunderstandings about protocol/delegate, but it’s normal when you start iOS development.
First of all, why does the app crash :
The variable delegate is an optional UserQuery. It’s okay for a delegate to be optional, but it’s never set in your code, so when you call :
self.delegate!.thisUser(username!)
you try to force unwrapping a nil variable, which results in the crash.
Protocols
Now, let’s talk about the protocol/delegate relationship.
You have an UICollectionViewController subclass, which embeds an UICollectionView object. This UICollectionView will be contains a mix of header, footer and cell. Your ProfileHeader class will thus be displayed within your UICollectionView.
In order to populate an UICollectionView, you don’t need to create your own protocol : there are already two protocols for this :
UICollectionViewDataSource is the main protocol to conforms to, because it allows you to populate the collection view
UICollectionViewDelegate is used for further customization of your tableview, i.e. customizing the appearance and handling events.
Since your Profile class inherits from UICollectionViewControlleryou don’t have to named these protocols after your class name since UICollectionViewController already conforms to these protocols as written in Apple docs
You will have to override the delegate and protocol methods in order to display some data. My advice is, before using headers and footers, to use only UICollectionViewCell objects for start easily.
By overriding the method -collectionView:numberOfItemsInSection: and - collectionView:cellForItemAtIndexPath:, you will be able to populate the collection view.
I have been pulling my hair out trying to get this 'Delegate' thing to work in Swift for an App I am working on.
I have two files: CreateEvent.swift and ContactSelection.swift, where the former calls the latter.
CreateEvent's contents are:
class CreateEventViewController: UIViewController, ContactSelectionDelegate {
/...
var contactSelection: ContactSelectionViewController = ContactSelectionViewController()
override func viewDidLoad() {
super.viewDidLoad()
/...
contactSelection.delegate = self
}
func updateInvitedUsers() {
println("this finally worked")
}
func inviteButton(sender: AnyObject){
invitedLabel.text = "Invite"
invitedLabel.hidden = false
toContactSelection()
}
/...
func toContactSelection() {
let contactSelection = self.storyboard?.instantiateViewControllerWithIdentifier("ContactSelectionViewController") as ContactSelectionViewController
contactSelection.delegate = self
self.navigationController?.pushViewController(contactSelection, animated: true)
}
ContactSelection's contents are:
protocol ContactSelectionDelegate {
func updateInvitedUsers()
}
class ContactSelectionViewController: UITableViewController {
var delegate: ContactSelectionDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate?.updateInvitedUsers()
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Stuff
self.delegate?.updateInvitedUsers()
}
}
What am I doing wrong? I am still new and don't fully understand this subject but after scouring the Internet I can't seem to find an answer. I use the Back button available in the Navigation Bar to return to my CreateEvent view.
var contactSelection: ContactSelectionViewController = ContactSelectionViewController()
This is instantiating a view controller directly, and the value never gets used. Since it looks like you're using storyboards, this isn't a good idea since none of the outlets will be connected and you'll get optional unwrapping crashes. You set the delegate of this view controller but that's irrelevant as it doesn't get used.
It also isn't a good idea because if you do multiple pushes you'll be reusing the same view controller and this will eventually lead to bugs as you'll have leftover state from previous uses which might give you unexpected outcomes. It's better to create a new view controller to push each time.
In your code you're making a brand new contactSelection from the storyboard and pushing it without setting the delegate.
You need to set the delegate on the instance that you're pushing onto the navigation stack.
It's also helpful to pass back a reference in the delegate method which can be used to extract values, rather than relying on a separate reference in the var like you're doing.
So, I'd do the following:
Remove the var contactSelection
Add the delegate before pushing the new contactSelection object
Change the delegate method signature to this:
protocol ContactSelectionDelegate {
func updateInvitedUsers(contactSelection:ContactSelectionViewController)
}
Change your delegate calls to this:
self.delegate?.updateInvitedUsers(self)
I don't think the title uses the right terminology so I'll try to clarify now.
I have two view controllers which i want to pass data between. view 1 has a tableView and the 2nd view has a MKMapView. Now in the corresponding controllers I want when you click on a cell in view 1 it sends you to the place on the map which the cell indicates eg. a New York cell would send to a map of new York. So I tried in the didSelectRowAtIndexPath that I create an instance of the second controller which would transfer the data to it. But when I did that it would always return a nil value in the second view controller.
So instead I created a 3rd swift file which has some global variables and when I transfer the data via them it works perfectly. Why is this so?
Cheers.
EDIT: Before the I went via a third file
Code for the ViewController 1
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var selectedCell = array[indexPath.row]
var viewController2 = ViewController2()
viewController2.textLabel1 = selectedCell.name
coord.textLabel2 = selectedCell.coordinate
}
Code for ViewController 2
#IBOutlet weak var textLabel1: UILabel!
#IBOutlet weak var textLabel2: UILabel!
Code for View Controller 2 going via 3rd file
#IBOutlet weak var textLabel1: UILabel!
#IBOutlet weak var textLabel2: UILabel!
override func viewDidLoad() {
textLabel1.text = globalVar1
textLabel2.text = globalVar2
}
Code from the 3rd File
var globalVar1: String!
var globalVar2: String!
So from the comments below I take it that in the first way the textLabels hadn't been initialised yet, so the values I assigned to them where turned into nil values. Is this correct? If so how would you do the first way correctly
If I had to guess, and I would because I cannot comment for more info yet(i get in trouble ).
It's because you are trying to assign it to an outlet.
The outlet has not been set yet which means when it is set (I think around ViewDidLoad)
the outlet will be set to nil.
The properties should however be retained if the object hasn't gone out of the heap that is.
PSedu code:
first view :
Your table view
Second View:
your map view
How to work
In you second view:
Add a NSMutableArray *valueArraytoGet in .h file,set its property
#synchronize it in .m file
Now in your first view at didSelectRowAtIndex method
create object of Second View Controller
and assign data as
SecondViewController *object=......
object.valueArraytoGet=[assign ur value array here]....
Hope it will help