Saving location coordinates to Core data- Swift 3 - ios

I want to save location coordinates into Core Data- what is the right way of storing these and retrieving them?
At the moment I am doing this and getting errors:
var newPlaceCoordinate = CLLocationCoordinate2D()
var newPlaceLatitude = CLLocationDegrees()
var newPlaceLongitude = CLLocationDegrees()
I get my coordinates from using Google maps API using the autocomplete widget.
let newPlaceCoordinate = place.coordinate
self.newPlaceCoordinate = place.coordinate
let newPlaceLatitude = place.coordinate.latitude
print(newPlaceLatitude)
self.newPlaceLatitude = place.coordinate.latitude
let newPlaceLongitude = place.coordinate.longitude
print(newPlaceLongitude)
self.newPlaceLongitude = place.coordinate.longitude
Then to store the coordinates I use:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let newPlace = NSEntityDescription.insertNewObject(forEntityName: "StoredPlace", into: context)
newPlace.setValue(newPlaceCoordinate, forKey: "coordinate")
newPlace.setValue(newPlaceLatitude, forKeyPath: "latitude")
newPlace.setValue(newPlaceLongitude, forKeyPath: "longitude")
And I have the attributes all set as String types.
Then to retrieve in my ViewDidLoad I have:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "StoredPlace")
request.returnsObjectsAsFaults = false
if let coordinate = result.value(forKey: "coordinate") as? String
{
let latitude = (coordinate as NSString).doubleValue
let longitude = (coordinate as NSString).doubleValue
let markers = GMSMarker()
markers.position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
markers.icon = GMSMarker.markerImage(with: UIColor.yellow)
markers.tracksViewChanges = true
markers.map = vwGMap
}
So this is not working and producing an error of 'attribute: property = "coordinate"; desired type = NSString; given type = NSConcreteValue'. I have a few questions.
How do I save a coordinate data appropriately?
Do I save it as a CLLocationDegrees or CLLocationCoordinate2D?
How do I convert these objects into NSString or should I be using a different type in my attributes? (I tried changing it to Double or Integer but it also produced errors). I have multiple location coordinates which I would want to store and retrieve from core data.

If you are going to deal with lots of data, then CoreData is the best bet.
These are my attributes in StorePlace entity, its best to save only latitude and longitude and create coordinates from them when needed in your implementation.
#NSManaged public var newPlaceLatitude: Double
#NSManaged public var newPlaceLongitude: Double
And to insert new data, I will do
class func insert(latitude: Double, longitude: Double) {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedObjectContext = appDelegate.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: "StoredPlace", in:managedObjectContext)
let newItem = StoredPlace(entity: entity!, insertInto: managedObjectContext)
newItem.newPlaceLatitude = latitude
newItem.newPlaceLongitude = longitude
do {
try managedObjectContext.save()
} catch {
print(error)
}
}
After retrieving latitude, longitude you can use coordinate in your implementations as
let newItem = getCoordinateFromCoreData() // your retrieval function
let coordinate = CLLocationCoordinate2D(latitude: newItem.latitude, longitude: newItem.longitude)

You need to convert the co-ordinate to double/NSdata type and then store it in CoreData. Use NSKeyedArchiver and NSKeyedUnarchiver to store and retrieve the values. CoreData can't store CLLocation object directly.
// Convert CLLocation object to Data
let newPlaceLatitudeArchived = NSKeyedArchiver.archivedDataWithRootObject(newPlaceLatitude)
//CoreData Piece
newPlace.setValue(newPlaceLatitudeArchived, forKeyPath: "latitude")
// fetch the latitude from CoreData as Archive object and pass it to unarchive to get the CLLocation
let newPlaceLatitudeUnArchived = NSKeyedUnarchiver.unarchiveObjectWithData(archivedLocation) as? CLLocation
You will need to convert you attribute to binary type in CoreData in this case.

Related

Why is "self" Blocking Data from CloudKit in my QueryOperation

I have multiple CKRecords in my database in Cloudkit. I turn those CKRecords into annotations for my map. Ever since I had to add self before my variable var annotation = MKPointAnnotation() in my queryoperation, it has only been loading one annotation to my Map. Why is that and how do I fix that??? Any help would be amazing!
How I fetch the records -
var points: [MKPointAnnotation] = []
var annotation = MKPointAnnotation()
let database = CKContainer.default().publicCloudDatabase
func fetchTruck() {
let truePredicate = NSPredicate(value: true)
let eventQuery = CKQuery(recordType: "User", predicate: truePredicate)
let queryOperation = CKQueryOperation(query: eventQuery)
queryOperation.recordFetchedBlock = { (record) in
self.points.append(self.annotation)
self.annotation.title = record["username"] as? String
self.annotation.subtitle = record["hours"] as? String
if let location = record["location"] as? CLLocation {
self.annotation.coordinate = location.coordinate
}
print("recordFetchedBlock: \(record)")
self.mapView.addAnnotation(self.annotation)
}
self.database.add(queryOperation)
}
After reviewing in more detail your code I think that the problem is that you are using always the same annotation. MKPointAnnotation is a class, a reference value, that means that every time you assing a value to self.annotation you're changing the reference, not creating a new one.
You're modifiying your app UI (mapView) inside the CKQueryOperation closure. Try to run the modification code in the main thread
Try something like...
var points: [MKPointAnnotation] = []
let database = CKContainer.default().publicCloudDatabase
func fetchTruck()
{
let truePredicate = NSPredicate(value: true)
let eventQuery = CKQuery(recordType: "User", predicate: truePredicate)
let queryOperation = CKQueryOperation(query: eventQuery)
queryOperation.recordFetchedBlock = { (record) in
var annotation = MKPointAnnotation()
annotation.title = record["username"] as? String
annotation.subtitle = record["hours"] as? String
if let location = record["location"] as? CLLocation
{
annotation.coordinate = location.coordinate
}
self.points.append(annotation)
DispatchQueue.main.async
{
self.mapView.addAnnotation(annotation)
}
print("recordFetchedBlock: \(record)")
}
self.database.add(queryOperation)
}

How to Make an Array for Annotations

Not all my annotations will show up in my map because I can't make var annotation = MKPoinatAnnotation the same MKPointAnnotation in fetching the CKrecords(annotation), and in getting directions for an annotation. I'm confused on how to make an array for annotations so I can be able to load all my annotations from the CloudKit database and be able to get directions when the annotation is selected.
let annotation = MKPointAnnotation()
let database = CKContainer.default().publicCloudDatabase
var truck: [CKRecord] = []
func fetch() {
let truePredicate = NSPredicate(value: true)
let eventQuery = CKQuery(recordType: "User", predicate: truePredicate)
let queryOperation = CKQueryOperation(query: eventQuery)
queryOperation.recordFetchedBlock = { (record : CKRecord!) in
self.truck.append(record)
self.annotation.title = record["username"] as? String
self.annotation.subtitle = record["hours"] as? String
if let location = record["location"] as? CLLocation {
self.annotation.coordinate = location.coordinate
}
print("recordFetchedBlock: \(record)")
self.mapView.addAnnotation(self.annotation)
}
self.database.add(queryOperation)
}
How I get directions -
#IBAction func getDirections(_ sender: Any) {
let view = annotation.coordinate
print("Annotation: \(String(describing: view ))")
let currentLocMapItem = MKMapItem.forCurrentLocation()
let selectedPlacemark = MKPlacemark(coordinate: view, addressDictionary: nil)
let selectedMapItem = MKMapItem(placemark: selectedPlacemark)
let mapItems = [selectedMapItem, currentLocMapItem]
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
MKMapItem.openMaps(with: mapItems, launchOptions:launchOptions)
}
I'm assuming the contents of the array change depending on the search query. If that is the case, make an array of type MKPointAnnotation:
var points: [MKPointAnnotation] = []
Then fill the array however you fill it and run it through a loop whereby each iteration adds a point to the map:
for point in points {
mapView.addAnnotation(point)
}
If you have a problem with varying types of annotations, make the array of type CLLocationCoordinate2D and then you can fill the array by accessing MKPointAnnotation.coordinate, for example.
Does this help?

Convert String from CoreData into CLLocationDegrees/ CLLocationCoordinate2D

I had been struggling to store CLLocationCoordinate2D data from markers on a google maps to CoreData. This cannot be done directly but I found a work around where I take the coordinates, split into CLLocationDegrees, convert it into a string text and store it. I do this by the following:
let marker = GMSMarker()
marker.position = CLLocationCoordinate2DMake(place.coordinate.latitude, place.coordinate.longitude)
let newPlaceLatitude = place.coordinate.latitude
print(newPlaceLatitude)
var latitudeText:String = "\(newPlaceLatitude)"
self.latitudeText = "\(newPlaceLatitude)"
let newPlaceLongitude = place.coordinate.longitude
print(newPlaceLongitude)
var longitudeText:String = "\(newPlaceLongitude)"
self.longitudeText = "\(newPlaceLongitude)"
Storing into CoreData:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let newPlace = NSEntityDescription.insertNewObject(forEntityName:
"StoredPlace", into: context)
newPlace.setValue(latitudeText, forKeyPath: "latitude")
newPlace.setValue(longitudeText, forKeyPath: "longitude")
However now I am struggling to reconstruct the strings back into CLLocationCoordinates. How would I turn a string to a CLLocationDegree/CLLocationCoordinate2D ? This is supposedly pretty simple but I have found that the following method doesn't work:
let latitude: CLLocationDegrees = Double(latitudeText)!
let longitude: CLLocationDegrees = Double(longitudeText)!
let markers = GMSMarker()
print(latitude)
print(longitude)
markers.position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
any other suggestions about how to change string to coordinate?
CLLocation's latitude and longitude are doubles, so with that in mind, you might consider having latitude and longitude properties that are doubles on your StoredPlace object. I called the properties coordinateX and coordinateY so it's easier to remember that they're custom coordinates, not "factory" properties.
You could create an extension in a file called StoredPlace+Extension.swift that looks like this:
import CoreData
import CoreLocation
extension StoredPlace {
func location() -> CLLocation {
let location = CLLocation(latitude: self.coordinateX, longitude: self.coordinateY)
return location
}
}
With this extension, you can then get the coordinates out of your results as follows:
for result in results {
print("coordinate = \(result.location().coordinate)")
print("latitude = \(result.location().coordinate.latitude)")
print("longitude = \(result.location().coordinate.longitude)")
}
You need to typecast your lat and long in decimal values, more preferable is double instead of float because of precision value which can drop pins at perfect locations.
Type casting in double using as keyword:
(yourCordinateString as NSString).doubleValue
casting in float values:
(yourCordinateString as NSString).floatValue

Pin an annotation to mapView with the saved coordinates in CoreData

I have searched now all over the internet and I have tried almost everything. My current project is:
I read the current coordinates from the User Location and save the latitude and longitude in CoreData and the name of this place(user can give a name)
// MARK: - AddPlace Button Events
#IBAction func onAddButton(sender: AnyObject) {
//searching the current location
let locCoord = CLLocationCoordinate2DMake(self.locationManager.location!.coordinate.latitude, self.locationManager.location!.coordinate.longitude)
// 1. pass the data from the current location to lat and lng
let lat = locCoord.latitude
let lng = locCoord.longitude
let ed = NSEntityDescription.entityForName("Store", inManagedObjectContext: moContext)
let store = Store(entity: ed!, insertIntoManagedObjectContext: moContext)
// 2. give the passed data to the array from CoreData
store.storeName = noteTextField.text
store.storeLng = lng
store.storeLat = lat
do {
// 3. save the informations in CoreData
try moContext.save()
noteTextField.text = ""
lat
lng
// 4. testing if the coordinates are saved - it works!
print(store.storeLat)
print(store.storeLng)
Now my problem is to display a pin on the saved place in another ViewController with a mapView. I tried:
UPDATE:
storeLat --> stored as Double
storeLng --> stored as Double
storeName --> stored as String
//MARK: - Second ViewController with a mapView
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Fetch Request
let fetchRequest = NSFetchRequest()
// Create Entity Description
let entityDescription = NSEntityDescription.entityForName("Store", inManagedObjectContext: self.moContext)
// Configure Fetch Request
fetchRequest.entity = entityDescription
do {
stores = try moContext.executeFetchRequest(fetchRequest) as! [Store]
if stores.count > 0
{
print("Test")
// add the annotation
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: store!.storeLat as! Double, longitude: store!.storeLng as! Double)
annotation.title = store?.storeName
self.mapView.addAnnotation(annotation)
} else
{
let alert = SCLAlertView()
alert.showError("Keine Places", subTitle: "Du hast noch keine Places gespeichert.")
}
} catch {
fatalError("Failed to fetch Places: \(error)")
}
Now it's going into the if - Statement. But I get a failure when I want to place the annotation. fatal error: unexpectedly found nil while unwrapping an Optional value. I think I have forget something to declare. Can someone help? Thank you already!
UPDATE 2
I found this fantastic site. But I have still some problems.

How to make value in func to global constant in Swift

How do I go about setting the long and latitude value (both which display a when app runs) to a global constant? The values are currently
String(format: "4.4f", latestLocation.coordinate.latitude)
and
String(format: "4.4f", latestLocation.coordinate.longitude)
I want to make these a global constant like userlatitude and userlongitude which can be accessed outside the function. Do I have to return someHope that makes sense.
func locationManager(manager: CLLocationManager!,
didUpdateLocations locations: [AnyObject]!)
{
var latestLocation = locations.last as! CLLocation
latitude.text = String(format: "%.4f",
latestLocation.coordinate.latitude)
longitude.text = String(format: "%.4f",
latestLocation.coordinate.longitude)
}
This way you can store Locations to NSUserDefaults:
//First Convert it to NSNumber.
let lat : NSNumber = NSNumber(double: Location.latitude)
let lng : NSNumber = NSNumber(double: Location.longitude)
//Store it into Dictionary
let locationDict = ["lat": lat, "lng": lng]
//Store that Dictionary into NSUserDefaults
NSUserDefaults.standardUserDefaults().setObject(locationDict, forKey: "Location")
After that you can access it this way anywhere you want:
//Access that stored Values
let userLoc = NSUserDefaults.standardUserDefaults().objectForKey("Location") as! [String : NSNumber]
//Get user location from that Dictionary
let userLat = userLoc["lat"]
let userLng = userLoc["lng"]
var Annotation = MKPointAnnotation()
Annotation.coordinate.latitude = userLat as! CLLocationDegrees //Convert NSNumber to CLLocationDegrees
Annotation.coordinate.longitude = userLng as! CLLocationDegrees //Convert NSNumber to CLLocationDegrees
One solution could be that you store the values as NSUserDefaults, which gives you access to the values when you need it. try this link for more information http://www.codingexplorer.com/nsuserdefaults-a-swift-introduction/
Define your Variable Below of import statement. Something Like,
import UIKit
let latitude = 4.0
// Do Stuff - This Variable access in Any class. Thanx Swift....
Another solution is put your value in .plist/NSUser Default and access in every class(Where should you wish to use) using appropriate key.

Resources