Persisting Geofence Events

To determine the time spent outside each day, we need the time the device exited and reentered the region. Let’s first create an enum type that helps to differentiate between entering and exiting. Add a new file to the project using the shortcut N, choose the Swift File template, and save it as UpdateType.swift. Add the following enum to the file:

 enum​ ​UpdateType​ : ​String​, ​Codable​ {
 case​ enter
 case​ exit
 }

enums in Swift let you define a type that can take a limited number of values, in this case either enter or exit. Add another Swift file, save it as RegionUpdate.swift, and add the following struct to it:

 struct​ ​RegionUpdate​ : ​Codable​ {
 let​ date: ​Date
 let​ updateType: ​UpdateType
 }

When the device enters or exits the geofence region, we want to store the current time and the type of the event. We could also store the type as a String value, but this is too fragile as it’s prone to misspellings. Using an enum type helps us avoid these kinds of errors.

Pro Tip: Let the Compiler Help You

images/aside-icons/tip.png

Make your developer life easier by using data structures the compiler can check. Wrap string-based information into enums and structs. Look into tools, such as SwiftGen, that can help with generating those data structures.

Note that the struct and the enum both conform to the protocol Codable. If all the properties in a type conforming to Codable also conform to Codable, instances of that type can easily be written to a file. All basic Swift types—such as String, Date, Int, and Float—already conform to Codable.

Now we can add an array to store the region updates. Open LocationProvider and add the highlighted property below the locationManager property:

 let​ locationManager: ​CLLocationManager
»var​ regionUpdates: [​RegionUpdate​] = []

We need a method that adds the region updates to that array. Add the following method to LocationProvider.

 func​ ​addRegionUpdate​(type: ​UpdateType​) {
 
 let​ lastUpdateType = regionUpdates.last?.updateType
 if​ type != lastUpdateType {
 let​ regionUpdate = ​RegionUpdate​(date: ​Date​(), updateType: type)
  regionUpdates.​append​(regionUpdate)
  }
 }

In this code we first check if the last element in the region updates array has a different update type than the one we want to add. We do this because we want to register only the first of the geofence events in a series of events with the same type. Technically, the event type should always alternate. But as we don’t know how geofences are implemented in iOS, it’s a good idea to make sure we get only the events we expect. If the new event type is different from the last, we create an instance of RegionUpdate and add it to the array.

We need to call this method when the device enters or leaves the monitored region. Add the highlighted line to locationManager(_:didEnterRegion:):

 func​ ​locationManager​(_ manager: ​CLLocationManager​,
  didEnterRegion region: ​CLRegion​) {
 
 printLog​(​"didEnterRegion: ​​(​​String​(describing: region)​)​​"​)
 
»addRegionUpdate​(type: .enter)
 }

Add a corresponding line to locationManager(_:didExitRegion:) with the type .exit:

 func​ ​locationManager​(_ manager: ​CLLocationManager​,
  didExitRegion region: ​CLRegion​) {
 
 printLog​(​"didExitRegion: ​​(​​String​(describing: region)​)​​"​)
 
»addRegionUpdate​(type: .exit)
 }

The region updates have to be stored somewhere and loaded the next time the app starts. The RegionUpdate type conforms to Codable. This means with a few lines of code, we can write the data into a JSON structure. JSON, short for JavaScript Object Notation, is a file format that is often used in iOS development because it’s relatively compact and easy to read.

The easiest way to store the data is to write it to our app’s documents directory, which is located in the app’s sandbox. Files in this directory stay there until the app is deleted or until the app deletes the files itself. To manage the file URL for the storage location of the region update data, we add an extension to FileManager.

Add a new Swift file to the project using the shortcut N and save it as FileManagerExtension.swift. Add the following extension to that file:

 extension​ ​FileManager​ {
 private​ ​static​ ​func​ ​documentsURL​() -> ​URL​ {
 guard​ ​let​ url = ​FileManager​.​default​.​urls​(
  for: .documentDirectory,
  in: .userDomainMask).first ​else​ {
 fatalError​()
  }
 return​ url
  }
 
 static​ ​func​ ​regionUpdatesDataPath​() -> ​URL​ {
 return​ ​documentsURL​().​appendingPathComponent​(​"region_updates.json"​)
  }
 }

The extension has two static methods. Static means that the methods belong to the type. To call these methods we don’t need to have an instance of FileManager.

The method documentsURL returns the file URL for the documents directory. It calls the method urls(for:in:) on the default file manager, which returns an array of file URLs. As we are asking for only one directory, we call first on the result array. If the array is empty, first returns nil; however, this should never happen because there is always a documents directory. Because our app doesn’t work without the ability to store the region updates, it will crash if there’s no documents directory.

In the method regionUpdatesDataPath, we use the documents directory to create the file URL for the JSON file region_updates.json. Now that we have the file URL to store the region update data, we can write the data to the file system.

Open LocationProvider.swift and add the following method to LocationProvider:

 func​ ​writeRegionUpdates​() {
 do​ {
 let​ data = ​try​ ​JSONEncoder​().​encode​(regionUpdates)
 try​ data.​write​(to: ​FileManager​.​regionUpdatesDataPath​(),
  options: .atomic)
  } ​catch​ {
 printLog​(​"error: ​​(​error​)​​"​)
  }
 }

This code uses an instance of JSONEncoder to encode the region updates array into a Data object. Next, the data is written to the file URL in the documents directory. Note that both statements are marked with the keyword try because they can throw an error. To catch the error, we embed the code in a do catch block. If one of the statements fails, the catch closure is executed and the error is passed in. For the moment, it’s enough to print the error to the console.

Pro Tip: Catch the Errors

images/aside-icons/tip.png

You can ignore the thrown error by using the try? or try! keyword. When an error is thrown, try? returns nil and try! leads to a crash. This means you throw away important information. Even if you’re not planning to show the error to the user, you should still use the built-in error handling when possible or at least print the error to the console to help your future self with debugging.

When a new region update is available, we want to write the collected data to the file. Add the highlighted line to the end of the if body in addRegionUpdate(type:):

 func​ ​addRegionUpdate​(type: ​UpdateType​) {
 
 let​ lastUpdateType = regionUpdates.last?.updateType
 if​ type != lastUpdateType {
 let​ regionUpdate = ​RegionUpdate​(date: ​Date​(), updateType: type)
  regionUpdates.​append​(regionUpdate)
 
»writeRegionUpdates​()
  }
 }

When the app starts, we need to load any stored region updates. Add the following method to LocationProvider:

 func​ ​loadRegionUpdates​() {
 do​ {
 let​ data = ​try​ ​Data​(contentsOf: ​FileManager​.​regionUpdatesDataPath​())
  regionUpdates = ​try​ ​JSONDecoder​().​decode​([​RegionUpdate​].​self​,
  from: data)
  } ​catch​ {
 printLog​(​"error: ​​(​error​)​​"​)
  }
 }

In this code we load the JSON data from the file in the documents directory. Next we use an instance of JSONDecoder to decode the data into an array of RegionUpdates. If a type conforms to the protocol Codable, an array with elements of that type also conforms to Codable. Again, these statements can throw an error, so we have to use the keyword try and wrap the calls in a do catch block.

Note that this method will print an error when we start the app the first time because there’s no file at that file URL yet.

A good time to load the stored region updates is right after creating the location provider. Add the highlighted line in init right below super.init:

 override​ ​init​() {
 
  locationManager = ​CLLocationManager​()
 
 super​.​init​()
 
»loadRegionUpdates​()
 
  locationManager.delegate = ​self
  locationManager.​requestAlwaysAuthorization​()
 }

In the next section, we’ll add the code to calculate how long the user spends outside the geofence region.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.141.202