Save Location in CoreData in background State with Swift

Hey guys! Some sort of interviewers repeatedly asking median level developer to track location and saving the data to CoreData as a task for hiring. Here is the solution for that. Here i shared how to track location and save it in coreData. Here i simply made some singleton classes and implement the logic.

  1. Create a template for our app.

2. Give name for your project with core data enabled.

One thing which we need to do is permission setting for accessing location. Add location privacy permission keys to info.plist file like below.

info.plist

Also for continuous fetch of location in background mode, we need to add background modes in signing&capabilities

signing&capabilities

Lets prepare for location fetch. For fetching current location, i created a singleton class to manage location updation.

class LocationManager: NSObject, CLLocationManagerDelegate{static var shared = LocationManager()var locationManager = CLLocationManager()}

then implement delegates and enable allowsBackgroundLocationUpdates in CLLocationManager instance.

class LocationManager: NSObject, CLLocationManagerDelegate{  static var shared = LocationManager()  var locationManager = CLLocationManager()
func initiateLocationManager(){ locationManager.requestAlwaysAuthorization() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.activityType = .other locationManager.allowsBackgroundLocationUpdates = true locationManager.requestAlwaysAuthorization() if CLLocationManager.locationServicesEnabled() { if #available(iOS 14.0, *) { switch locationManager.authorizationStatus { case .notDetermined, .restricted, .denied: print("No access") case .authorizedAlways, .authorizedWhenInUse: print("Access") @unknown default: break } } else { if CLLocationManager.locationServicesEnabled() { switch CLLocationManager.authorizationStatus() { case .notDetermined, .restricted, .denied: print("No access") case .authorizedAlways, .authorizedWhenInUse: print("Access") @unknown default: print("fatal error") } } else { print("Location services are not enabled") } } } else { print("Location services are not enabled") } }
}

Add essential delegates to CLLocationManagerDelegate inherited in the above singleton class.

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {print(locations.last?.coordinate.latitude ?? 0.0)print(locations.last?.coordinate.longitude ?? 0.0)}func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {print(error.localizedDescription)}

Now create singleton class to handle CRUD functions using CoreData.

struct CoreDataManager{static var shared = CoreDataManager()}

Create entity in xcdatamodel file like below. I added attributes like lat, long, address and date.

attributes

Confirm the name in AppDelegate.swift file under persistentContainer variable.

Create enums named CoreDataEnum so that we didn't miss the spelling for keys in core data to save data:

enum CoreDataEnum: String{case entityname = "Location"case lat = "lat"case long = "long"case address = "address"case date = "date"}

Lets create a struct for fetching location data after saved. Here is the LocationData struct

struct LocationData{var lat: Decimal? //Decimal type which is given in entityvar long: Decimal?var address: String?var date: Date?}

Lets write a function that helps to create rows in SQL using CoreData. Note that CoreData is just a framework not Database.

//MARK: Createfunc createData(entity: String?, locdata: LocationData){   let managedContext = appDelegate?.persistentContainer.viewContext   if managedContext != nil{   let userEntity = NSEntityDescription.entity(forEntityName: entity   ?? "", in: managedContext!)!   let product = NSManagedObject(entity: userEntity, insertInto: managedContext)   product.setValue(locdata.lat, forKeyPath: CoreDataEnum.lat.rawValue)   product.setValue(locdata.long, forKey: CoreDataEnum.long.rawValue)   product.setValue(locdata.address, forKey: CoreDataEnum.address.rawValue)   product.setValue(locdata.date, forKey: CoreDataEnum.date.rawValue)   do {         try managedContext?.save()      } catch let error as NSError {         print("Could not save. \(error), \(error.userInfo)")      }  }}

Now add function for Read.

//MARK: Readmutating func retriveData(entity: String?) -> [LocationData]{    var data = [NSManagedObject]()    var loc = LocationData()    let managedContext = appDelegate?.persistentContainer.viewContext    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity ?? "")    do {       let result = try managedContext?.fetch(fetchRequest)       if result?.count ?? 0 > 0{       for data in result as! [NSManagedObject] {       print(data.value(forKey: CoreDataEnum.address.rawValue) as? String ?? "unknown")      }    data = result  as! [NSManagedObject]    for entity in data{    loc.lat = entity.value(forKey: CoreDataEnum.lat.rawValue) as? Decimal ?? 0.0    loc.long = entity.value(forKey: CoreDataEnum.long.rawValue) as? Decimal ?? 0.0     loc.address = entity.value(forKey: CoreDataEnum.address.rawValue) as? String ?? ""     loc.date = entity.value(forKey: CoreDataEnum.date.rawValue) as? Date ?? Date()     fetchedLocData.append(loc)    }  }} catch {  print("Failed")  }  return fetchedLocData}

Now add function update.

//MARK: Updatefunc updateData(entity: String?, locData: LocationData){  let managedContext = appDelegate?.persistentContainer.viewContext  let fetchRequest:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: entity ?? "")  fetchRequest.predicate = NSPredicate(format: "date = %@", locData.date! as CVarArg)  do{    let test = try managedContext?.fetch(fetchRequest)    let objectUpdate = test?[0] as! NSManagedObject    objectUpdate.setValue(locData.lat, forKey: CoreDataEnum.lat.rawValue)     objectUpdate.setValue(locData.long, forKey: CoreDataEnum.long.rawValue)     objectUpdate.setValue(locData.address, forKey: CoreDataEnum.address.rawValue)      objectUpdate.setValue(locData.date, forKey: CoreDataEnum.date.rawValue)   do{    try managedContext?.save()    }   catch{     print(error)   } } catch{   print(error) }}

Now add function Delete.

//MARK: Deletefunc deleteData(entity: String, loc: LocationData){    let managedContext = appDelegate?.persistentContainer.viewContext    let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity)    fetchRequest.predicate = NSPredicate(format: "date = %@", loc.date! as CVarArg)    do{      let test = try managedContext?.fetch(fetchRequest)      let objectToDelete = test?[0] as! NSManagedObject      managedContext?.delete(objectToDelete)    do{      try managedContext?.save()     }    catch{       print(error)     }   }   catch{      print(error)   }}

Now add the code to save location data in didUpdateLocations delegate.

//MARK: didUpdateLocationsfunc locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {print(locations.last?.coordinate.latitude ?? 0.0)print(locations.last?.coordinate.longitude ?? 0.0)var locDta = LocationData()locDta.lat = Decimal(locations.last?.coordinate.latitude ?? 0.0)locDta.long = Decimal(locations.last?.coordinate.longitude ?? 0.0)locDta.date = Date()getAddress(location: locations.last!, completion: { address inlocDta.address = addressCoreDataManager.shared.createData(entity: CoreDataEnum.entityname.rawValue, locdata: locDta)})}

Now the location save the data to CoreData. Lets make a separation for specifying updating location in background. Add in sceneDidEnterBackground.

DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async {LocationManager.shared.startupdateLocation()self.countdownTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(self.updateLocation), userInfo: nil, repeats: true)RunLoop.current.add(self.countdownTimer!, forMode: .common)RunLoop.current.run()}

Now add in sceneWillEnterForeground.

self.countdownTimer = nilLocationManager.shared.stopUpdatingLocation()

Now create table view for displaying locations.

class ViewController: UIViewController{@IBOutlet weak var tableView: UITableView!var locData = CoreDataManager.shared.fetchedLocDataoverride func viewDidLoad() {super.viewDidLoad()NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)// Do any additional setup after loading the view.}override func viewWillAppear(_ animated: Bool) {super.viewWillAppear(animated)willEnterForeground()}}

Now add delegate methods.

extension ViewController: UITableViewDelegate, UITableViewDataSource{func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {return locData.count}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as? LocationCellcell?.locationText.text = locData[indexPath.row].addresscell?.timeText.text = dateToString(date: locData[indexPath.row].date ?? Date())return cell!}}

Add these at bottom.

extension ViewController{func dateToString(date: Date) -> String{let dateFormatter = DateFormatter()dateFormatter.dateFormat = "MM-DD-YYYY HH:mm"return dateFormatter.string(from: date)}@objc func willEnterForeground() {locData = CoreDataManager.shared.retriveData(entity: "Location")tableView.reloadData()}}

Now all the setup are ready. Run the code and test. Any queries comment below.

HAPPY CODING🌎

Passionate iOS developer chasing my dreams toward success