Create a clickable map preview using MKMapSnapshotter - swift

I have a map that currently shows the user's current location with no pin to show them exactly where they are. I want to make the map a photo of where the user's set location is. I only know how to show where their device is and need them to be able to set their base of operations. I don't need specific addresses. I just need the city where they reside.
I then need the image to be able to be tapped on. When tapped, the image makes the MapKit full screen and interactive. They can then zoom around the map and see where other users' set their base of operations.
I am new to coding and can't figure out how to allow the user to set a permanent location even if they move around the country. I also don't know how to set up the mapsnapshot and on top of that expand when tapped to show a fully working map view.
I am only currently able to ask if I can activate location services and then show their map view where they are when it is loaded. Here is the code:
import UIKit
import CoreLocation
import MapKit
class HomeTableViewController: UITableViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, CLLocationManagerDelegate, MKMapViewDelegate {
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
#IBOutlet weak var mapPreviewImageView: UIImageView!
#IBOutlet weak var mapView: MKMapView!
let manager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
manager.desiredAccuracy = kCLLocationAccuracyBest // battery
manager.delegate = self
manager.requestWhenInUseAuthorization()
manager.startUpdatingLocation()
// Always adopt a light interface style.
overrideUserInterfaceStyle = .light
takeSnapShot()
}
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return 7
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
if let location = locations.first {
manager.stopUpdatingLocation()
render(location)
}
}
func render (_ location: CLLocation) {
let coordinate = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapView.setRegion(region, animated: true)
}
func takeSnapShot() {
let location = CLLocation()
let mapSnapshotOptions = MKMapSnapshotter.Options()
let coordinate = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapSnapshotOptions.region = region
// Set the scale of the image. We'll just use the scale of the current device, which is 2x scale on Retina screens.
mapSnapshotOptions.scale = UIScreen.main.scale
// Show buildings and Points of Interest on the snapshot
mapSnapshotOptions.showsBuildings = true
mapSnapshotOptions.mapType = .satellite
let snapShotter = MKMapSnapshotter(options: mapSnapshotOptions)
snapShotter.start() { snapshot, error in
guard let snapshot = snapshot else {
return
}
self.mapPreviewImageView.image = snapshot.image
}
}
}
Thanks for your help in advance. I really need to make some progress on this app and I can't seem to find any tutorials or web results on how to do this.
Edit:
I have tried adding a function that turns my UIImage into the snapshot. I am able to return an image but it doesn't show my location and it is smaller than my UIImage. I had edited the code above to reflect the changes I made. I don't know what I am doing wrong.

In your example, you are creating a CLLocationManager, but not using it. You are using CLLocation(). That obviously has no (meaningful) coordinate associated with it. Make sure to supply a valid coordinate. For example, have didUpdateLocations call takeSnapshot:
class ViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
private weak var snapshotter: MKMapSnapshotter?
private lazy var manager: CLLocationManager = {
let manager = CLLocationManager()
manager.delegate = self
manager.distanceFilter = 20
return manager
}()
override func viewDidLoad() {
super.viewDidLoad()
if manager.authorizationStatus == .notDetermined {
manager.requestWhenInUseAuthorization()
}
manager.startUpdatingLocation()
}
func takeSnapshot(of location: CLLocation) {
snapshotter?.cancel() // cancel prior one, if any
let options = MKMapSnapshotter.Options()
options.camera = MKMapCamera(lookingAtCenter: location.coordinate, fromDistance: 1000, pitch: 0, heading: 0)
options.mapType = .satellite
options.size = imageView.bounds.size
let snapshotter = MKMapSnapshotter(options: options)
snapshotter.start() { snapshot, _ in
self.imageView.image = snapshot?.image
}
self.snapshotter = snapshotter
}
}
extension ViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last(where: { $0.horizontalAccuracy >= 0 } ) else { return }
takeSnapshot(of: location)
}
}
That yields:
Unrelated observations:
You are using MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1). I personally do not find spans in degrees to be terribly useful. I might advise using meters, e.g.
options.region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 1000, longitudinalMeters: 1000)
Or use a MKMapCamera:
options.camera = MKMapCamera(lookingAtCenter: location.coordinate, fromDistance: 1000, pitch: 0, heading: 0)
There is no point in using showsBuildings if you are using a map type of satellite. The docs say:
The mapType property must be set to MKMapType.standard for extruded buildings to be displayed.
I do not believe that you have to set the scale. The docs say:
This property is set to a default value that corresponds to the resolution of the current device’s display.
Besides, this property is now deprecated, anyway.
I would suggest, though, to set the size of the image.

Related

Swift 3 and Firebase - Cannot add annotations with firebase array, prints to console though

In my console, I can access Firebase latitude, longitude, and name of restaurant. I have a few print statements that I use to test that I am getting the values I needed. When I try to assign these values into an annotation.coordinate = CLLocationCoordinate2DMake(with the respective info here) I still can't get this function to print in maps.
I mainly created a function so that I can call it in viewDidLoad() so that everything I want automatically comes up when this page of the app loads.
I also created an action for a button so that when a user clicks a button on shown on the view controller, it also prints the location but THAT case also is not working. When I make coordinates I also do not know if I need ! on res.latitude and res.longitude!…. When I take it off it still does not work. A previous project I created I added annotations the same way I did here MINUS the firebase array. I created one myself with struct variables (title, latitude, longitude) in them that are later called.
import UIKit
import Firebase
import FirebaseStorage
import FirebaseDatabase
import MapKit
import CoreLocation
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {//add last 2 delegates/protocols.conforming
//firebase refrences
var dataBaseRef: FIRDatabaseReference! {
return FIRDatabase.database().reference()
}
var storageRef: FIRStorageReference! {
return FIRStorage.storage().reference()
}
var restaurantArray = [Restaurant]()
#IBOutlet weak var mapView: MKMapView!
#IBOutlet weak var segments: UISegmentedControl!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
title = "Maps"
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Main Menu", style: .plain, target: self, action: #selector(SSASideMenu.presentLeftMenuViewController))
self.locationManager.delegate = self//as soon as loaded find location--conforms to delegate
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest//best location
self.locationManager.requestWhenInUseAuthorization()//only want location when using app
self.locationManager.startUpdatingLocation()//turn on location manager..make location start looking
self.mapView.showsUserLocation = true//shows blue dot
displayRestaurants()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
fetchRestaurants()
displayRestaurants()
}
//see if you want to give it a try!!!!!!!!!!!!!!!
func fetchRestaurants(){
FIRDatabase.database().reference().child("AthensRestaurants/Restaurants").observe(.value, with: { (snapshot) in
var results = [Restaurant]()
for res in snapshot.children{
let res = Restaurant(snapshot: res as! FIRDataSnapshot)
results.append(res)
}
self.restaurantArray = results
}) { (error) in
print("error encountered dumbass")
print(error.localizedDescription)
}
}
//work here!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
func displayRestaurants(){
//array already created--using a firebase array
for res in restaurantArray {
let annotation = MKPointAnnotation()
annotation.title = res.name
print(res.name)
let x = res.latitude //shows that this works and I can retrieve data!!!
print (x! as Double)
let y = res.longitude
print(y! as Double)
annotation.coordinate = CLLocationCoordinate2D(latitude: res.latitude!, longitude: res.longitude!) //Should the exclamation marks be there "!"
mapView.addAnnotation(annotation)
}
}
//A way of testing
#IBAction func test12(sender: UIButton) {
displayRestaurants() //another way i tried makiing anotations show....!!!!!!!!!
}
//below works on seperate projects
//MARK: - Location Delegate Methods
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {//didupdate is contiously called so below is continuously called
let location = locations[0]
let span = MKCoordinateSpanMake(0.01, 0.01)
let myLocation:CLLocationCoordinate2D = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
let region = MKCoordinateRegionMake(myLocation, span)//lat long--region that we want map to scope to--parameters is closeness zoom
self.mapView.setRegion(region, animated: true)//since we have thise we can stop updating eventually
self.locationManager.stopUpdatingLocation()
}
//check for errors
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {//should be NSError but
print("Errors:" + error.localizedDescription)
//displayRestaurants()
}
//segment changer for terrain, hybrid, and regular---just allows different types of map views
#IBAction func segChange(_ sender: Any) {
switch segments.selectedSegmentIndex {
case 0:
mapView.mapType = MKMapType.standard
break
case 1:
mapView.mapType = MKMapType.satellite
break
case 2:
mapView.mapType = MKMapType.hybridFlyover
break
default:
break
}
}
}

Swift MapView Stuck Around User's Current Location

Currently, my code drops a pin on the user's current location. There is one small problem when I try to move the map around, because the view will shift back and be centered around that current location pin. I want the user be able to navigate the map and move it around, and if the user switches view controllers (goes to another tab) and comes back, the map will be centered around the user location pin. I have been trying to modify this code to do this, but I have not had any luck where to start.
import UIKit
import MapKit
import CoreLocation
let newPin = MKPointAnnotation()
class MapVC: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
// User's location
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
if #available(iOS 8.0, *) {
locationManager.requestAlwaysAuthorization()
} else {
// Fallback on earlier versions
}
locationManager.startUpdatingLocation()
// add gesture recognizer
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(MapVC.mapLongPress(_:))) // colon needs to pass through info
longPress.minimumPressDuration = 1.5 // in seconds
//add gesture recognition
map.addGestureRecognizer(longPress)
}
// func called when gesture recognizer detects a long press
func mapLongPress(_ recognizer: UIGestureRecognizer) {
print("A long press has been detected.")
let touchedAt = recognizer.location(in: self.map) // adds the location on the view it was pressed
let touchedAtCoordinate : CLLocationCoordinate2D = map.convert(touchedAt, toCoordinateFrom: self.map) // will get coordinates
let newPin = MKPointAnnotation()
newPin.coordinate = touchedAtCoordinate
map.addAnnotation(newPin)
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
map.removeAnnotation(newPin)
let location = locations.last! as CLLocation
let center = CLLocationCoordinate2D(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
//set region on the map
map.setRegion(region, animated: true)
newPin.coordinate = location.coordinate
map.addAnnotation(newPin)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Your code has several problems:
You declare a variable newPin at global scope and in mapLongPress(...) you declare a new variable let newPin = ... locally so the global newPin isn't used.
In didUpdateLocations() you first remove the (global) newPin annotation (why??) and set it again at the end of the function. Because the global newPin was never set to anything useful this will never get the desired result.
Furthermore, in didUpdateLocations() you set the map's region and center point to the current location. This is done on every location update, giving weird results when trying to pan the map.
To set center and region when the view appears, try something like that:
class MapVC: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var map: MKMapView!
let locationManager = CLLocationManager()
// class variable for the current location
var lastLocation: CLLocation?
override func viewDidLoad() {
// ...
}
override func viewDidAppear(_ animated: Bool) {
if self.lastLocation != nil {
// set center and region to current location
let center = CLLocationCoordinate2D(latitude: self.lastLocation.coordinate.latitude, longitude: self.lastLocation.coordinate.longitude)
let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
//set region on the map
map.setRegion(region, animated: true)
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.lastLocation = locations.last
}
}

How to send current location to another iPhone user swift?

I want to be able to send my current location to another iPhone user. I can get my current location and continuously update my location to see where I am on the map but how could I send this info to another iPhone user so they can see where i am in real time?
The basic structure of code is:
import UIKit
import MapKit
class ViewController: UIViewController,CLLocationManagerDelegate {
#IBOutlet weak var myMapView: MKMapView!
let myLocMgr = CLLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
myLocMgr.desiredAccuracy = kCLLocationAccuracyBest
myLocMgr.requestWhenInUseAuthorization()
myLocMgr.startUpdatingLocation()
myLocMgr.delegate = self
}
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
// get most recient coordinate
let myCoor = locations[locations.count - 1]
//get lat & long
let myLat = myCoor.coordinate.latitude
let myLong = myCoor.coordinate.longitude
let myCoor2D = CLLocationCoordinate2D(latitude: myLat, longitude: myLong)
//set span
let myLatDelta = 0.05
let myLongDelta = 0.05
let mySpan = MKCoordinateSpan(latitudeDelta: myLatDelta, longitudeDelta: myLongDelta)
let myRegion = MKCoordinateRegion(center: myCoor2D, span: mySpan)
//center map at this region
myMapView.setRegion(myRegion, animated: true)
//add anotation
let myAnno = MKPointAnnotation()
myAnno.coordinate = myCoor2D
myMapView.addAnnotation(myAnno)
}

MKTileOverlay - adding in bundle tiles

Adding an overlay with a tiled map is supposed to be easy. I'm having a great problem. They don't show.
I have map tiles of the center of Bayeux in 15 16 and 17 zoom in a folder baymap.
I slid the folder into the project.
here's the code
import UIKit
import MapKit
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
var latitude: CLLocationDegrees = 0.0
var longitude: CLLocationDegrees = 0.0
var cnt: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
self.mapView.mapType = MKMapType.Standard
//Map centre
let centre = CLLocationCoordinate2D(latitude: 49.275,
longitude: -0.7028)
//Declare span of map
let span = MKCoordinateSpan(latitudeDelta: 0.01,
longitudeDelta: 0.01)
//Set region of the map
let region = MKCoordinateRegion(center: centre, span: span)
self.mapView.setRegion(region, animated: false)
self.mapView.regionThatFits(region)
//Get the URL template to the map tiles
let baseURL = NSBundle.mainBundle().bundleURL.absoluteString
let urlTemplate = baseURL.stringByAppendingString("baymap/{z}/{x}/{y}.png/")
print(urlTemplate)
let carte_indice = MKTileOverlay(URLTemplate:urlTemplate)
carte_indice.geometryFlipped = true
carte_indice.canReplaceMapContent = false
self.mapView.addOverlay(carte_indice)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(mapView: MKMapView!, rendererForOverlay overlay: MKOverlay!) -> MKOverlayRenderer!
{
print("call overlay")
if overlay is MKTileOverlay
{
print("is MKTileoverlay")
var renderer = MKTileOverlayRenderer(overlay:overlay)
renderer.alpha = 0.8
return renderer
}
return nil
}
}
I've edited the question to add print "call overlay" and "is MKTileoverlay".
They both print in the console.
The apple map of Bayeux shows up fine but no overlay.
In the console i'm getting dozens of errors like this which refer to tiles not in the bundle.
: Error loading URL file:///Users/colinmcgarry/Library/Developer/CoreSimulator/Devices/5A9A20A4-9C3F-4A65-8823-9721463FF985/data/Containers/Bundle/Application/D2C87A46-E848-4C0D-9B05-30E731EC037F/TileOverlaystack.app/baymap/17/65283/86214.png/: Error Domain=NSURLErrorDomain Code=-1100 "The requested URL was not found on this server."
can anyone see what I'm doing wrong. Is there a good tutorial some place for in bundle tile overlay in Swift?
thanks
Found the solution.
carte_indice.geometryFlipped = true
should be false.
depends on the tile system.

Swift programming: Multiple Positions on Map View

I have a simple problem and I am new to IOS - I want to have multiple GPS streams coming on the same map. Please help as to how could that be done
the current code works:
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet weak var mapView: MKMapView!
let locationManager = CLLocationManager()
override func viewDidLoad()
{
super.viewDidLoad()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
self.mapView.showsUserLocation = true
self.mapView.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
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.01, longitudeDelta: 0.01))
self.mapView.setRegion(region, animated: true)
self.locationManager.stopUpdatingLocation()
}
func locationManager(manager: CLLocationManager, didFailWithError error: NSError)
{
print("Errors: " + error.localizedDescription)
}
}
but this only gives me one location.
You have a map displayed, and on that map you want to display two locations.
The first location is that of the device, so you get the co-ordinates from the device's GPS.
The second location is somebody else's position. Where are you getting their co-ordinates from? You cannot access their GPS. The app on their device must write their co-ordinates to a server somewhere. You app then reads these co-ordinates from the server and displays them using a MKPointAnnotation. There is no "GPS stream" for the app other than that for the device it is running on.
Similarly, the app on your device must write your own co-ordinates, so it can appear on their app.
If you don't have a server, you could investigate using Apple's CloudKit (for example).