Having trouble retrieving data from CloudKit - ios

I am having trouble fetching the locations from cloudkit. The location gets uploaded, but when I try to have them printed out and loaded, they aren't downloaded. I don't get any errors.
This function uploads the location to CloudKit:
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.015, longitudeDelta: 0.015))
self.mapView.setRegion(region, animated: true)
self.locationManager.stopUpdatingLocation()//
let locationRecord = CKRecord(recordType: "location")
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { record, error in
}
if error == nil
{
print("Location saved")
}
event1 = locations
}
This function fetches the locations from CloudKit:
func loadLocation()
{
let locations = [CKRecord]()
let publicData1 = CKContainer.defaultContainer().publicCloudDatabase
let query1 = CKQuery(recordType: "location", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray:nil))
publicData1.performQuery(query1, inZoneWithID: nil) { (results: [CKRecord]?, error: NSError?) -> Void in
if let locations = results
{
self.locations = locations
print(locations)
}
}
}

So to do this I made a unit test, that passes:
//
// CloudKitLocationsTests.swift
//
import XCTest
import UIKit
import CoreLocation
import CloudKit
class CloudKitLocationsTests: XCTestCase {
let locations = [ CLLocation(latitude: 34.4, longitude: -118.33), CLLocation(latitude: 32.2, longitude: -121.33) ]
func storeLocationToCloud(location:CLLocation) {
let locationRecord = CKRecord(recordType: "location")
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { (records, error) in
if error != nil {
print("error saving locations: \(error)")
} else {
print("Locations saved: \(records)")
}
}
}
func fetchLocationsFromCloud(completion: (error:NSError?, records:[CKRecord]?) -> Void) {
let query = CKQuery(recordType: "Location", predicate: NSPredicate(value: true))
CKContainer.defaultContainer().publicCloudDatabase.performQuery(query, inZoneWithID: nil){
(records, error) in
if error != nil {
print("error fetching locations")
completion(error: error, records: nil)
} else {
print("found locations: \(records)")
completion(error: nil, records: records)
}
}
}
func testSavingLocations(){
let testExpectation = expectationWithDescription("saveLocations")
var n = 0
for location in self.locations {
let locationRecord = CKRecord(recordType: "Location")
locationRecord["location"] = location
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { (records, error) in
if error != nil {
print("error saving locations: \(error)")
} else {
print("Locations saved: \(records)")
}
n += 1
if n >= self.locations.count {
testExpectation.fulfill()
}
}
}
// do something then call fulfill (in callback)
waitForExpectationsWithTimeout(10){ error in
if error != nil {
XCTFail("timed out waiting on expectation: \(testExpectation)")
}
}
}
func testFetchingLocations(){
let testExpectation = expectationWithDescription("FetchLocations")
fetchLocationsFromCloud(){ (error, records) in
if error != nil {
XCTFail("error fetching locations")
} else {
XCTAssertGreaterThan(records!.count, 0)
}
// do something then call fulfill (in callback)
testExpectation.fulfill()
}
waitForExpectationsWithTimeout(10){ error in
if error != nil {
XCTFail("timed out waiting on expectation: \(testExpectation)")
}
}
}
}
Note that you had case mismatch Location/location. Also, I am doing a subscript to set the field value.
Run this it works. Getting the location from the location manger callback has nothing to do with CloudKit so you should be able to plug this in as you require.
One other thing: I did turn on the option to allow you to query on ID field for the Location record type.

If your problem is to retrieve an array of CLLocation, try this:
publicData1.performQuery(query1, inZoneWithID: nil) { records, error in
var locations = [CLLocation]()
if let records = records {
for record in records {
if let location = record["location"] as? CLLocation {
locations.append(location)
}
}
}
}

Related

Domain=kCLErrorDomain Code=8 when fetching location through zipcode

I'm trying to fetch the the latitude and longitude based on the input parameters postal/city and country code. Below is my code, this works fine if enter City and country name but shows error if I enter zipcode and country code. Below is the code. (Note: Location services and app permissions are enabled)
func getLocationFrom(postalCityCode: String, countryCode: String) -> CLLocation? {
let geocoder = CLGeocoder()
var location: CLLocation?
let address = CNMutablePostalAddress()
address.postalCode = postalCityCode
address.country = countryCode
geocoder.geocodePostalAddress(address, preferredLocale: Locale.current) { (placemarks, error) in
guard error == nil else {
print("Error: \(error!)")
return
}
guard let placemark = placemarks?.first else {
print("Error: placemark is nil")
return
}
guard let coordinate = placemark.location?.coordinate else {
print("Error: coordinate is nil")
return
}
location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
print("Found location = \(location)")
}
return location
}
Working input: Shanghai, CN
Failing input: 200040, CN
Edit
Attached updated code as suggested in the answer but still experiencing same issue
Currently, you are using return location before it is set, since geocoder.geocodePostalAddress(...) is an asynchronous function.
That means you need to use a completion handler (for example) to return the location, when it has the results, something like this:
func getLocationFrom(postalCityCode: String, countryCode: String, completion: #escaping ( CLLocation?) -> Void) {
let geocoder = CLGeocoder()
var location: CLLocation?
let address = CNMutablePostalAddress()
address.postalCode = postalCityCode
address.country = countryCode
geocoder.geocodePostalAddress(address, preferredLocale: Locale.current) { (placemarks, error) in
guard error == nil else {
print("Error: \(error!)")
return completion(nil)
}
guard let placemark = placemarks?.first else {
print("Error: placemark is nil")
return completion(nil)
}
guard let coordinate = placemark.location?.coordinate else {
print("Error: coordinate is nil")
return completion(nil)
}
location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
print("\n--> Found location = \(location) \n")
completion(location) // <-- here
}
}
Use it like this:
getLocationFrom(postalCityCode: "200040", countryCode: "CN") { location in
print("\n---> location: \(location) \n")
}
EDIT-1
for testing and isolating the issue, try this code in a new SwiftUI project:
struct ContentView: View {
#State var cityLocation = CLLocation()
var body: some View {
Text(cityLocation.description)
.onAppear {
getLocationFrom(postalCityCode: "200040", countryCode: "CN") { location in
print("\n---> location: \(location) \n")
if let theLocation = location {
cityLocation = theLocation
}
}
}
}
func getLocationFrom(postalCityCode: String, countryCode: String, completion: #escaping ( CLLocation?) -> Void) {
let geocoder = CLGeocoder()
var location: CLLocation?
let address = CNMutablePostalAddress()
address.postalCode = postalCityCode
address.country = countryCode
geocoder.geocodePostalAddress(address, preferredLocale: Locale.current) { (placemarks, error) in
guard error == nil else {
print("Error: \(error!)")
return completion(nil)
}
guard let placemark = placemarks?.first else {
print("Error: placemark is nil")
return completion(nil)
}
guard let coordinate = placemark.location?.coordinate else {
print("Error: coordinate is nil")
return completion(nil)
}
location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
print("\n--> Found location = \(location) \n")
completion(location)
}
}
}

CLPlacemark has no member 'get'

I am referring one func to get user's location and the code is below:
func updateLocation(_ userLocation: CLLocation) {
if (userLocation.horizontalAccuracy > 0) {
self.locationService.stopUpdatingLocation()
return
}
self.latitude = NSNumber(value: userLocation.coordinate.latitude as Double)
self.longitude = NSNumber(value: userLocation.coordinate.longitude as Double)
if !self.geocoder.isGeocoding {
self.geocoder.reverseGeocodeLocation(userLocation, completionHandler: {(placemarks, error) in
if let error = error {
logger.error("reverse geodcode fail: \(error.localizedDescription)")
return
}
if let placemarks = placemarks, placemarks.count > 0 {
// TODO:
let onePlacemark = placemarks.get(index: 0)
self.address = "\(onePlacemark?.administrativeArea,onePlacemark?.subLocality,onePlacemark?.thoroughfare)"
self.city = (onePlacemark?.administrativeArea!)!
self.street = (onePlacemark?.thoroughfare!)!
}
})
}
}
When building the project, it threw something "CLPlacemark has no member 'get'" at this line:
let onePlacemark = placemarks.get(index: 0)
I am writing the project with swift 4.0, and
import Foundation
import INTULocationManager
is done.

Trying to update CLLocation if NSUserDefault Exists in Swift

Originally I was trying to save a CLLocation as a NSUserDefault value before I stored it as a CKRecord in CLoudkit but I got the error: "defaults.setObject(locationRecord.recordID, forKey: "locationRecordID")" with the reason being "Attempt to set a non-property-list object as an NSUserDefaults/CFPreferences value for key locationRecordID". So now I am trying to save the lat and long as a default and replace the old location in Cloudkit. I am currently getting a 'Thread 1 SIGABRT' error on the line "publicDB.fetchRecordWithID((defaults.objectForKey("Location") as! CKRecordID),completionHandler: " with the reason being "Could not cast value of type '__NSCFDictionary' (0x1a1bec968) to 'CKRecordID'."
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
{
let location = locations.last
self.loc1 = location!
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.mapView.setRegion(region, animated: true)
self.locationManager.stopUpdatingLocation()
self.locationManager.startMonitoringSignificantLocationChanges()
self.getRecordToUpdate(locations)
let lat: CLLocationDegrees = center.latitude
self.lat1 = lat
let long: CLLocationDegrees = center.longitude
self.long1 = long
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { record, error in
}
if error == nil
{
print("Location saved")
}
}
func getRecordToUpdate(locations:[CLLocation])
{
let defaults = NSUserDefaults.standardUserDefaults()
var locationRecord:CKRecord
if defaults.objectForKey("Location") == nil{
locationRecord = CKRecord(recordType: "location")
let locationDict = ["lat": lat1, "lng": long1]//
defaults.setObject(locationDict, forKey: "Location")//
self.updateLocationRecord(locationRecord, locations: locations)
print("new")
}else{
let publicDB = CKContainer.defaultContainer().publicCloudDatabase
publicDB.fetchRecordWithID((defaults.objectForKey("Location") as! CKRecordID),completionHandler: {
(record, error) in
if error == nil{
self.updateLocationRecord(record!, locations: locations)
print("fetched record")
}else{
print("Error fetching previous record")
}
})
}
}
func updateLocationRecord(locationRecord:CKRecord, locations:[CLLocation])
{
let location = locations.last
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { record, error in
}
if error == nil
{
print("Location saved")
}
}
Try this
You can save NSData to NSUserDefaults. So all you need is to convert your CLLocation object to NSData as follow:
// saving your CLLocation object
let locationData = NSKeyedArchiver.archivedDataWithRootObject(your location here)
NSUserDefaults.standardUserDefaults().setObject(locationData, forKey: "locationData")
// loading it
if let loadedData = NSUserDefaults.standardUserDefaults().dataForKey("locationData") {
if let loadedLocation = NSKeyedUnarchiver.unarchiveObjectWithData(loadedData) as? CLLocation {
println(loadedLocation.coordinate.latitude)
println(loadedLocation.coordinate.longitude)
}
}

Deleting CKRecord from Cloudkit in swift

I am trying to have the users' location be updated by this function. Originally it was saving the users' location every time, but I added the code that will be below the comment. How can I make sure the record that is being deleted is the old location?
let locationRecord = CKRecord(recordType: "location")
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()//
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { record, error in
}
if error == nil
{
print("Location saved")
self.loc1 = location!
}
//testing code below
let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: [locationRecord.recordID])
operation.savePolicy = .AllKeys
operation.modifyRecordsCompletionBlock = { added, deleted, error in
if error != nil {
print(error)
} else {
print("location updated")
}
}
CKContainer.defaultContainer().publicCloudDatabase.addOperation(operation)
}
The best way to do this would be to update the existing one, as #Michael pointed out. The easy way to do this would be the first time you create the record, save the recordID to the defaults. For example,
func getRecordToUpdate(locations:[CLLocation])->CKRecord?{
let defaults = NSUserDefaults.standardUserDefaults()
var locationRecord:CKRecord
if defaults.objectForKey("locationRecordID") == nil{
//Create a new record and save its id
locationRecord = CKRecord(recordType: "location")
defaults.setObject(locationRecord.recordID, forKey: "locationRecordID")
self.updateLocationRecord(locationRecord, locations)
}else{
//Fetch the already existing record
let publicDB = CKContainer.defaultContainer().publicCloudDatabase
publicDB.fetchRecordWithID((defaults.objectForKey("locationRecordID") as! CKRecordID), completionHandler{
(record, error) in
if error == nil{
self.updateLocationRecord(record, locations)
}else{
print("Error fetching previous record")
}
}
}
}
Create a new function,
func updateLocationRecord(locationRecord:CKRecord, locations:[CLLocation]){
//Put your code from "let location..." to just above the testing code
//You may want to fix the block syntax in the call to saveRecord
}
Then in the original locationManager method, take out everything and put
self.getRecordsToUpdate(locations)

How to use query to fetch cloudkit data in swift

I am trying to have all the users' locations stored in cloudkit, then downloaded by each device. I marked on the code in storeLocation where I get the error:
"Cannot convert value of type '(NSError?, [CKRecord]?)' (aka '(Optional, Optional>)') to expected argument type '(error: NSError?, records: [CKRecord]?) -> Void'"
//saves location in cloud kit //currently works well:
var locArray = [CKRecord]()
func storeLocation(location:CLLocation) {
let locationRecord = CKRecord(recordType: "location")
locationRecord.setObject(location, forKey: "location")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(locationRecord) { (records, error) in
if error != nil {
print("error saving locations: \(error)")
} else {
print("Locations saved: \(records)")
loadLocation((error, self.locArray)) //where I get error******
}
}
}
//fetches location from cloud kit:
func loadLocation(completion: (error:NSError?, records:[CKRecord]?) -> Void)
{
let query = CKQuery(recordType: "Location", predicate: NSPredicate(value: true))
CKContainer.defaultContainer().publicCloudDatabase.performQuery(query, inZoneWithID: nil){
(records, error) in
if error != nil {
print("error fetching locations")
completion(error: error, records: nil)
} else {
print("found locations: \(records)")
completion(error: nil, records: records)
}
}
}
I believe instead of:
loadLocation((error, self.locArray))
You need to call this:
loadLocation() { (error, records) in
// do something here with the returned data.
}

Resources