Total noob here to Swift 3. All I need to do is pass a double value from one ViewController to another via the 2 user input text fields. I've tried numerous solutions and have read everything I can find on passing data between ViewControllers. I get a 'fatal error: unexpectedly found nil while unwrapping an Optional value'. I have a real hard time understand the wrapping and unwrapping of the variables and I'm sure it's something simple.
Here is my first ViewController:
import UIKit
var longitude: Double?
var latitude: Double?
class ViewController: UIViewController {
#IBOutlet var getLongitude: UITextField!
#IBOutlet var getLatitude: UITextField!
#IBOutlet var mapbutton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func MapBtn(_ sender: Any) {
performSegue(withIdentifier: "segue", sender: self)
longitude = Double(getLongitude.text!)!
latitude = Double(getLatitude.text!)!
}
}
And this is the SecondViewController:
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let camera = GMSCameraPosition.camera(withLatitude: latitude!, longitude: longitude!, zoom: 6.0)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
view = mapView
// Creates a marker in the center of the map.
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
marker.map = mapView
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Avoid global mutable state. Send data from one scene to another.
Try this:
class ViewController: UIViewController {
#IBOutlet weak var longitudeTextField: UITextField!
#IBOutlet weak var latitudeTextField: UITextField!
#IBOutlet weak var mapButton: UIButton!
#IBAction func didTapToMapButton(_ sender: UIButton) {
// I assume your storyboard' name is Main. If not, change it below accordingly
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
// I assume your destination view controller' identifier and type is SecondViewController. If not, change it below accordingly.
if let secondViewController = mainStoryboard.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController {
if let longitude = Double(longitudeTextField.text),
let latitude = Double(latitudeTextField.text) {
secondViewController.latitude = latitude
secondViewController.longitude = longitude
}
present(secondViewController, animated: true, completion: nil)
}
}
}
class SecondViewController: UIViewController {
var latitude: Double?
var longitude: Double?
override func viewDidLoad() {
super.viewDidLoad()
if let latitude = latitude,
let longitude = longitude {
let camera = GMSCameraPosition.camera(withLatitude: latitude, longitude: longitude, zoom: 6.0)
let mapView = GMSMapView.map(withFrame: CGRect.zero, camera: camera)
view = mapView
// Creates a marker in the center of the map.
let marker = GMSMarker()
marker.position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
marker.map = mapView
}
}
}
1st, Unwrapping. when you try to unwrap the variable, rather forcely unwrap it, it's better test if it's nil, or you could use if let to safely unwrap it.
2nd, pass variable from one VC to another. As you know, in iOS, those VCs instances are like any other variable. All you need to do is trying to fetch a reference to that VC, then assign the Double to its accessible variable. Take a look at this link
You could create a segue between the two controllers and pass the variable by overriding prepare for segue.
Pass that variable to the new controller and load that information to its outlet on viewDidAppear or viewWillAppear.
self.prepare(for: "segueName", sender: self)
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueName" {
if let vc = segue.destination as? DestinationViewController {
// Pass Data to New View Controller
}
}
}
Related
I am new to Swift and iOS development. I have an issue where I have three viewControllers connected via a tab bar navigation controller. The first viewController is empty for now and not in use. In the secondViewController I update the UI with data from a weather API. In the thirdViewController I have a map. When I access the map I need the coordinates to update the url in the secondViewController.
What I'm trying to achieve is retrieving the current latitude and longitude in the thirdViewController (no problem), but I'm unable to send these values through a delegate in order to trigger the function in the secondViewController and thus update the variables in there. The delegate function in the secondViewController just doesn't get triggered. What am I doing wrong? Or is it even possible to do this way?
The code is as follows for both viewControllers:
secondViewController (this is where I need the values from the map in thirdViewController)
import UIKit
class SecondViewController: UIViewController, MapViewControllerDelegate {
var forecastData: [ForecastData] = []
var currentLat: String = "59.911166"
var currentLon: String = "10.744810"
#IBOutlet weak var tableView: UITableView!
var weatherManager = WeatherManager()
var mapViewController = ThirdViewController()
override func viewDidLoad() {
super.viewDidLoad()
mapViewController.delegate = self
weatherManager.delegate = self
weatherManager.fetchWeather(latitude: currentLat, longitude: currentLon)
tableView.backgroundView = UIImageView(image: UIImage(named: "background"))
tableView.dataSource = self
tableView.register(UINib(nibName: "WeatherCell", bundle: nil), forCellReuseIdentifier: "WeatherCell")
}
func viewDidAppear() {
weatherManager.fetchWeather(latitude: currentLat, longitude: currentLon)
}
// THIS FUNCTION SHOULD BE TRIGGERED BUT DOESN'T
func mapViewController(latitude: String, longitude: String) {
currentLat = latitude
currentLon = longitude
print(currentLat)
print(currentLon)
print("DELEGATE METHOD TRIGGERED")
}
}
//MARK: - WeatherManagerDelegate
extension SecondViewController: WeatherManagerDelegate {
func didUpdateWeather(_ weatherManager: WeatherManager, weather: WeatherModel) {
forecastData.append(ForecastData(timespan: "Nå", precipitation: "", tempOrWeather: "Temperatur", tempWeatherLabel: "\(weather.temperatureNow)" + " " + "\(weather.tempUnits)"))
forecastData.append(ForecastData(timespan: "Neste Time", precipitation: "\(weather.precipitationNext1H)" + " " + "\(weather.precipUnits)", tempOrWeather: "vær", tempWeatherLabel: String(weather.conditionNext1H) ))
forecastData.append(ForecastData(timespan: "Neste 6 timer", precipitation: "\(weather.precipitationNext6H)" + " " + "\(weather.precipUnits)", tempOrWeather: "vær", tempWeatherLabel: String(weather.conditionNext6H)))
forecastData.append(ForecastData(timespan: "Neste 12 timer", precipitation: "", tempOrWeather: "vær", tempWeatherLabel: String(weather.conditionNext12H)))
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
func didFailWithError(error: Error) {
print(error)
}
}
extension SecondViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return forecastData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "WeatherCell", for: indexPath)
as! WeatherCell
cell.TimeframeLabel.text = forecastData[indexPath.row].TimeframeLabel
cell.TempLabel.text = forecastData[indexPath.row].TempLabel
cell.WeatherCodeLabel.text = forecastData[indexPath.row].WeatherCodeLabel
cell.PrecipitationLabel.text = forecastData[indexPath.row].PrecipitationLabel
return cell
}
}
thirdViewController (this is where I try to pass the coordinates values through the delegate)
import UIKit
import MapKit
import CoreLocation
protocol MapViewControllerDelegate {
func mapViewController(latitude: String, longitude: String)
}
class ThirdViewController: UIViewController, MKMapViewDelegate {
var currentLat = ""
var currentLon = ""
#IBOutlet weak var mMapView: MKMapView!
fileprivate let locationManager: CLLocationManager = CLLocationManager()
var delegate: MapViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
locationManager.requestWhenInUseAuthorization()
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = kCLDistanceFilterNone
locationManager.startUpdatingLocation()
mMapView.showsUserLocation = true
currentLat = String((locationManager.location?.coordinate.latitude)!)
currentLon = String((locationManager.location?.coordinate.longitude)!)
print(currentLat)
print(currentLon)
//delegate?.mapViewController(latitude: "TESTlon", longitude: "TESTlati")
}
override func viewDidAppear(_ animated: Bool) {
updateWeatherView()
}
// THIS SHOULD TRIGGER THE DELEGATE FUNCTION IN SECOND VIEW CONTROLLER AND PASS THE DATA ALONG
func updateWeatherView() {
delegate?.mapViewController(latitude: "TESTlon", longitude: "TESTlati")
}
}
I have tried different thing, but nothing seems to work, the delegate function in secondViewController just doesn't get triggered. Again, I am very new to this so maybe it's a simple thing. Thanks in advance for any help! Also, here is a screenshot of my storyboard if it can help:
As #Duncan C said, doing
var weatherManager = WeatherManager()
var mapViewController = ThirdViewController()
just creates a new instance of those view controllers -- these are completely unrelated to the ones you already have. Sure, if you do mapViewController.delegate = self, you are setting mapViewController's delegate... but mapViewController is not the view controller that you want.
To get the view controller that you want, you can access the tab bar controller's view controllers (these are automatically created for you when you make them in the storyboard), and pick the right one:
if
let viewControllers = self.tabBarController?.viewControllers,
viewControllers.count >= 3,
let thirdViewController = viewControllers[2] as? ThirdViewController /// [2] because that's the third view controller in the array, which is the one you want
{
thirdViewController.delegate = self
}
self.tabBarController could be nil, so you need to unwrap it. Then, you should make sure that there are more than 3 view controllers. Finally, you need to cast the view controller you need (self.tabBarController?.viewControllers[2]) to ThirdViewController. You can then set the delegate to that view controller.
I am using MapKit on Xcode to create a "nearby" hospital/mental health center/call center location app. I want the user to be able to filter the map's results. For instance, if I am looking for hospitals, I only want to see hospital locations appear on the map. So far, to do this, I created a sliding hamburger menu with checkbox-like buttons to use as filters. So for example, you tap the hamburger menu button and you check the box with hospitals so that you only get hospital results on the map. However, I must have messed it up somehow because when I get to the MapViewController, only a black screen appears. Please advise how I can fix this or maybe use a different method entirely to achieve the same goal.
import UIKit
import MapKit
class MapViewController: NSObject, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
#IBOutlet weak var trailingMap: NSLayoutConstraint!
#IBOutlet weak var leadingMap: NSLayoutConstraint!
func viewDidLoad() {
mapView.delegate = self as? MKMapViewDelegate
let locationManager = CLLocationManager()
let regionRadius:CLLocationDistance = 1000
locationManager.requestWhenInUseAuthorization()
mapView.showsUserLocation = true
locationManager.startUpdatingLocation()
}
var hamburgerMenuIsVisible = false
#IBAction func hamburgerMenu(_ sender: Any) {
if hamburgerMenuIsVisible {
leadingMap.constant = 150
trailingMap.constant = -150
hamburgerMenuIsVisible = true
} else {
leadingMap.constant = 0
trailingMap.constant = 0
hamburgerMenuIsVisible = false
}
}
var mapItems: [MKMapItem] = []
var place = MKLocalSearch.Request() //user's facility search options
var naturalLanguageQuery: String? {
place.naturalLanguageQuery = "hospital"
let place2 = MKLocalSearch.Request()
place2.naturalLanguageQuery = "Mental Health Facility"
let place3 = MKLocalSearch.Request()
place3.naturalLanguageQuery = "Women's Center"
let place4 = MKLocalSearch.Request()
place4.naturalLanguageQuery = "Call Center"
place.region = self.mapView.region
place2.region = self.mapView.region
place3.region = self.mapView.region
place4.region = self.mapView.region
return place.naturalLanguageQuery
}
#IBAction func hospitalButton(_ sender: Any) {
print(mapItems)
}
#IBAction func callCenterButton(_ sender: Any) {
print(mapItems)
}
}
Your ViewController is subclass of UIViewController, so you have to use inheritance from UIViewController instead of NSObject
So, replace this
class MapViewController: NSObject, CLLocationManagerDelegate {
with this
class MapViewController: UIViewController, CLLocationManagerDelegate {
also fix your viewDidLoad method by adding override keyword before func keyword of your viewDidLoad function. That's because you override function from UIViewController. You will have to call super.viewDidLoad() at the begining of this function. So viewDidLoad should look like this
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self as? MKMapViewDelegate
let locationManager = CLLocationManager()
let regionRadius:CLLocationDistance = 1000
locationManager.requestWhenInUseAuthorization()
mapView.showsUserLocation = true
locationManager.startUpdatingLocation()
}
I have a UIViewController, inside of this one in my storyboard I have a UITableViewController and a GMSMapView.
Well, I have a service to populate my UITableViewController, with some information and georeferences. The UITableViewController behavior is working perfectly, but when I try to access to my UIViewController the markers aren't added.
class RequestViewController: UIViewController {
#IBOutlet weak var mapView: GMSMapView!
var solicitudes = [SolicitudesModel]()
override func viewDidLoad() {
super.viewDidLoad()
let camera = GMSCameraPosition.camera(withLatitude: 83.4824182, longitude: -88.1776567, zoom: 15)
self.mapView.camera = camera
let marker = GMSMarker()
marker.position = CLLocationCoordinate2DMake(13.4824182, -88.1776567)
marker.title = "My location"
marker.map = self.mapView
}
}
The marker as this point was added.
class RequestTableViewController: UITableViewController {
var reqs = [RequestModel]()
#IBOutlet var RequestTable: UITableView!
override func viewDidLoad() {
var requestVC = RequestViewController()
Alamofire.request(UrlGlobals.retriveInformation()).responseJSON { response in
let json = JSON(response.result.value)
var i: Int = 0
self.reqs.removeAll()
for _ in json.array ?? [] {
//Some code to populate the table
let marker = GMSMarker()
marker.position = CLLocationCoordinate2DMake(CLLocationDegrees(json["lat"].float!), CLLocationDegrees(json["lng"].float!))
marker.title = "Some title"
marker.map = requestVC.mapView
}
self.RequestTable.reloadData()
}
}
}
The json retrieves all the information correctly. How I can access to my primary ViewController and add those google markers?
Right now your RequestViewController is an instance that exists only in viewDidLoad. Make it a class property and you will be able to access it anywhere.
I am aware that there are many other questions like this however I assure you this is not a duplicate as far as I can tell. As you can see in the code below I have not marked any values as Optional however I keep getting this error. It crashes when I run the snapNext value highlighting the
viewMap.camera = newLocation
Here is the full code below, I have the viewMap linked to just a regular UIView
import UIKit
import MapKit
import GoogleMaps
class ViewController: UIViewController {
var camera = GMSCameraPosition.cameraWithLatitude(33.600727, longitude: -117.900840, zoom: 16.9)
#IBOutlet weak var viewMap: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
viewMap.camera = camera
viewMap = GMSMapView.mapWithFrame(CGRectZero, camera: camera)
viewMap.myLocationEnabled = true
viewMap.settings.myLocationButton = true
let marker = GMSMarker()
marker.position = CLLocationCoordinate2DMake(33.600727, -117.900840)
marker.title = "Newport Beach"
marker.snippet = "California"
marker.map = viewMap
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func snapNext(sender: AnyObject) {
let newLocation = GMSCameraPosition.cameraWithLatitude(33.622578, longitude: -117.911099, zoom: 16.9)
viewMap.camera = newLocation
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Your error is in the line:
viewMap = GMSMapView.mapWithFrame(CGRectZero, camera: camera)
Here you are setting the map to a new anonymous object and because the map property is weak it will be set to nil automatically as soon as the anonymous object goes out of scope (i.e. when viewDidLoad completes).
Either you want to create the object in the storyboard; in which case leave it as a weak outlet, or you just want it as a 'normal' property; in which case remove the weak and IBOutlet.
import UIKit
import MapKit
import GoogleMaps
class ViewController: UIViewControllerGMSMapViewDelegate , CLLocationManagerDelegate {
var camera:GMSCameraPosition!
#IBOutlet weak var viewMap: GMSMapView!
override func viewDidLoad() {
super.viewDidLoad()
camera = GMSCameraPosition.cameraWithLatitude(33.600727, longitude: -117.900840, zoom: 16.9)
viewMap.camera = camera
viewMap = GMSMapView.mapWithFrame(CGRectZero, camera: camera)
viewMap.myLocationEnabled = true
viewMap.delegate=self
viewMap.settings.myLocationButton = true
let marker = GMSMarker()
marker.position = CLLocationCoordinate2DMake(33.600727, -117.900840)
marker.title = "Newport Beach"
marker.snippet = "California"
marker.map = viewMap
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func snapNext(sender: AnyObject) {
camera = GMSCameraPosition.cameraWithLatitude(33.622578, longitude: -117.911099, zoom: 16.9)
viewMap.camera = camera
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
As you've already pointed out the viewMap is nil. Why it's getting released, I do not know (Need more info to know that). But I know that you can prevent it by removing the weak keyword.
#IBOutlet weak var viewMap: GMSMapView!
However, this might lead to leaks if you have retain cycles between your viewMap and your view controller.
I am working on a small map application, so far I have the code to drop map pins and save them so they remain once the app is reopened, this class is for the main view controller:
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, UISearchBarDelegate, UIPopoverPresentationControllerDelegate {
var location: CLLocation!
let locationManager = CLLocationManager()
#IBOutlet weak var placesMap: MKMapView!
#IBOutlet weak var addButton: UIBarButtonItem!
#IBOutlet weak var moreStuff: UIButton!
// Popover button action
#IBAction func moreStuff(sender: AnyObject) {
self.performSegueWithIdentifier("showMoreStuff", sender:self)
moreStuff.adjustsImageWhenHighlighted = false
}
#IBAction func addButton(sender: AnyObject) {
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: self.placesMap.userLocation.coordinate.latitude, longitude: self.placesMap.userLocation.coordinate.longitude)
self.placesMap.addAnnotation(annotation)
self.locationManager.startUpdatingLocation()
}
// Location function
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations.last
let center = CLLocationCoordinate2D(latitude: location!.coordinate.latitude, longitude: location!.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.004, longitudeDelta: 0.004))
self.placesMap?.setRegion(region, animated: true)
self.locationManager.stopUpdatingLocation()
let locationDictionary:[String:Double] = ["latitude":center.latitude,"longitude":center.longitude]
var locationArray = [[String:Double]]()
if NSUserDefaults.standardUserDefaults().objectForKey("locationArray") != nil {
locationArray = NSUserDefaults.standardUserDefaults().objectForKey("locationArray") as! [[String:Double]]
}
locationArray.append(locationDictionary)
NSUserDefaults.standardUserDefaults().setObject(locationArray, forKey: "locationArray")
NSUserDefaults.standardUserDefaults().synchronize()
}
override func viewDidLoad() {
super.viewDidLoad()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
self.placesMap?.showsUserLocation = true
if NSUserDefaults.standardUserDefaults().objectForKey("locationArray") != nil {
for dictionary in NSUserDefaults.standardUserDefaults().objectForKey("locationArray") as! [[String:Double]]{
let center = CLLocationCoordinate2D(latitude: dictionary["latitude"]!, longitude: dictionary["longitude"]!)
let annotation = MKPointAnnotation()
annotation.coordinate = center
self.placesMap?.addAnnotation(annotation)
}
}
}
I want to add a button which allows all pins (memories) to be reset at once. This button is located on a new scene and class, called "PopoverOptions". I have the following code from the class which should do this, but it does not seem to be functioning as no pins disappear from the map once it is pressed by the user!
#IBOutlet weak var resetMemories: UIButton!
#IBAction func resetMemories(sender: AnyObject) {
func removeStoredLocations(){
NSUserDefaults.standardUserDefaults().removeObjectForKey("locationArray")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
Any idea why the pins aren't being removed? I have ensured that the class is linked correctly as well as the buttons outlet / action.
You clear out your user defaults key, but don't change the map view on the view controller that shows those pins. You need to call removeAnnotations to remove the annotations from the map.
You could add a viewWillAppear method that detects that your annotations have been deleted and remove them. Something like this:
func viewWillAppear(_ animated: Bool)
{
if NSUserDefaults.standardUserDefaults().objectForKey("locationArray") == nil
{
if let annotations = self.placesMap.annotations
self.placesMap?.removeAnnotations(annotations)
}
}
The code above is written to only remove the annotations from the map if the entire annotations dictionary was deleted from userDefaults. That way you avoid reloading the annotations every time the view controller regains focus.
BTW, you said your new scene is called "PopoverOptions." If the scene is presented in a popover then the above might not work. (I don't think viewWillAppear gets called on a view controller when a popover is dismissed, but I don't remember for sure.)