I am using MapKit, and the user has the ability to add annotations. They can tap on the screen, which prompts them if they want to add an annotation with a UIAlert, and If they say yes, it presents another view controller so the user can input information about the annotation, like the location name, description, etc. That view controller has a 'Done' BarButtonItem up top to confirm the information that they input, and create the annotation.
@IBAction func doneButtonPressed(_ sender: UIBarButtonItem) {
doneButtonHasBeenPressed = true
dismiss(animated: true, completion: nil)
}
The problem is, The annotation has to be created in my 'touchesEnded' function in the original view controller, that sent the user to the view controller where they input the annotation information, because that is where it gets the CLCoordinate2D from (using the touchesEnded). It is in that same touchesEnded function where I send the user to that next view controller. Here is the touchesEnded code and a helper function it uses:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location(in: view)
// Converts CGPoint coordinates and UIView to CLLocationCordinate2D (map coordinates) Remember to rename if addButtonPressed order of creation of annotation gets changed!
let coordinatesTouchedToCreateAnnotation = mapView.convert(touchLocation, toCoordinateFrom: view)
if userIsAllowedToAddAnnotation {
let alertController = UIAlertController(title: "Confirm", message: "Are you sure you want to add a jump location here?", preferredStyle: .alert)
let noAction = UIAlertAction(title: "No", style: .cancel, handler: nil)
let yesAction = UIAlertAction(title: "Yes", style: .default) { (action) in
// Segue takes user to JumpSpotCreatorController to input information about the jump location
self.performSegue(withIdentifier: "CreateJumpSpot", sender: self)
if self.jumpSpotCreatorController.doneButtonHasBeenPressed == true {
self.jumpSpotCreatorController.doneButtonHasBeenPressed = false
self.createJumpSpotAnnotation(coordinatesDeterminedByTouch: coordinatesTouchedToCreateAnnotation)
self.userIsAllowedToAddAnnotation = false
self.tapToAddJumpSpotLabel.isHidden = true
}
}
alertController.addAction(noAction)
alertController.addAction(yesAction)
present(alertController, animated: true, completion: nil)
} else {
return
}
}
}
// Force unwrap is okay because this will only be called if 'Done' button is pressed in the JumpSpotCreatorController, which mandates that those inputs not be nil.
func createJumpSpotAnnotation(coordinatesDeterminedByTouch: CLLocationCoordinate2D) {
mapView.addAnnotation(JumpSpotAnnotation(name: jumpSpotCreatorController.nameTextField.text!, coordinate: coordinatesDeterminedByTouch, estimatedHeight: jumpSpotCreatorController.estimatedHeightTextField.text!, locationDescription: jumpSpotCreatorController.descripitionTextView.text, warnings: jumpSpotCreatorController.warningsTextView.text ?? "", image: jumpSpotCreatorController.jumpSpotImageView.image ?? UIImage(imageLiteralResourceName: "Image-1")))
}
As you can see, the block of code that creates the Annotation in the touchesEnded function (located right above where I add actions to the alertController, in case you can't find it. It's about 4 lines), is executed immediately, as opposed to when I need it to be, which is once the 'Done' button is pressed in my other view controller (JumpSpotCreatorController). I tried fixing that with the doneButtonHasBeenPressed variable, but it makes no difference (for obvious reasons). How can I execute it only once that done button is pressed? I can't initialize the other view controller as an object in the main one (the one with touchesEnded is the main one) because it will create an infinite loop of references between the two view controllers. Can DispatchQueue help in some way? I've researched it for hours but can't quite figure out how to apply it here. Thanks a lot.
There are a number of ways to do this --- each with pros and cons.
This approach uses the delegate / protocol pattern.
We define a protocol, which will allow classes to execute functions in their delegate classes:
// protocol / delegate pttern
protocol JumpSpotDelegate: class {
func createJumpSpotAnnotation(_ name: String, estimatedHeight: String, locationDescription: String, warnings: String, image: UIImage?)
func cancelAnnotation()
}
In the controller with your map view, we conform to that delegate:
class MapViewController: UIViewController, JumpSpotDelegate {
In JumpSpotCreatorController
, we set up a delegate
property:
class JumpSpotCreatorController: UIViewController {
weak var delegate: JumpSpotDelegate?
When we navigate to the JumpSpotCreatorController
, we assign self as its delegate:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// make sure we're acting on the correct segue
if segue.identifier == "CreateJumpSpot", let vc = segue.destination as? JumpSpotCreatorController {
// set the delegate in the JumpSpotCreatorController we're navigating to
vc.delegate = self
}
}
In JumpSpotCreatorController
, when Done button is tapped, we tell the map controller via the delegate function:
delegate?.createJumpSpotAnnotation(name, estimatedHeight: estimatedHeight, locationDescription: description, warnings: warnings, image: img)
Here it is together. I added objects via let
statements to write this... I expect you have them as @IBOutlet
connections:
// protocol / delegate pttern
protocol JumpSpotDelegate: class {
func createJumpSpotAnnotation(_ name: String, estimatedHeight: String, locationDescription: String, warnings: String, image: UIImage?)
func cancelAnnotation()
}
class MapViewController: UIViewController, JumpSpotDelegate {
// this will hold the touch point while we navigate to and back from the JumpSpotCreatorController
var lastTouch: CLLocationCoordinate2D?
let mapView: MKMapView!
var userIsAllowedToAddAnnotation = true
let tapToAddJumpSpotLabel = UILabel()
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let touchLocation = touch.location(in: view)
// Converts CGPoint coordinates and UIView to CLLocationCordinate2D (map coordinates)
// store in class property
self.lastTouch = mapView.convert(touchLocation, toCoordinateFrom: view)
if userIsAllowedToAddAnnotation {
let alertController = UIAlertController(title: "Confirm", message: "Are you sure you want to add a jump location here?", preferredStyle: .alert)
let noAction = UIAlertAction(title: "No", style: .cancel, handler: nil)
let yesAction = UIAlertAction(title: "Yes", style: .default) { (action) in
// Segue takes user to JumpSpotCreatorController to input information about the jump location
self.performSegue(withIdentifier: "CreateJumpSpot", sender: self)
}
alertController.addAction(noAction)
alertController.addAction(yesAction)
present(alertController, animated: true, completion: nil)
} else {
return
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// make sure we're acting on the correct segue
if segue.identifier == "CreateJumpSpot", let vc = segue.destination as? JumpSpotCreatorController {
// set the delegate in the JumpSpotCreatorController we're navigating to
vc.delegate = self
}
}
// called by Button action in JumpSpotCreatorController
func createJumpSpotAnnotation(_ name: String, estimatedHeight: String, locationDescription: String, warnings: String, image: UIImage?) {
// the coordinate parameter was stored in our class property "lastTouch"
guard let lastTouch = self.lastTouch else {
// self.lastTouch was not set!
return
}
mapView.addAnnotation(JumpSpotAnnotation(name: name, coordinate: lastTouch, estimatedHeight: estimatedHeight, locationDescription: description, warnings: warnings, image: image ?? UIImage(imageLiteralResourceName: "Image-1")))
self.userIsAllowedToAddAnnotation = false
self.tapToAddJumpSpotLabel.isHidden = true
// pop from JumpSpotCreatorController back to self
self.navigationController?.popViewController(animated: true)
}
// I'm assuming you would also have a Cancel button?
func cancelAnnotation() {
self.lastTouch = nil
// pop from JumpSpotCreatorController back to self
self.navigationController?.popViewController(animated: true)
}
}
class JumpSpotCreatorController: UIViewController {
weak var delegate: JumpSpotDelegate?
let nameTextField = UITextField()
let estimatedHeightTextField = UITextField()
let descripitionTextView = UITextView()
let warningsTextView = UITextView()
let jumpSpotImageView = UIImageView()
@IBAction func doneBtnTapped() {
// presumably, you'll validate all these
guard let name = nameTextField.text,
let estimatedHeight = estimatedHeightTextField.text,
let description = descripitionTextView.text,
let warnings = warningsTextView.text,
let img = jumpSpotImageView.image else {
// notify user required fields were blank
return
}
delegate?.createJumpSpotAnnotation(name, estimatedHeight: estimatedHeight, locationDescription: description, warnings: warnings, image: img)
}
@IBAction func cancelBtnTapped() {
delegate?.cancelAnnotation()
}
}