This post is part of the Singapore Breathe Development Diary Series.


Before we get into this week’s update, some repository updates:

  • A README has been added
  • Continuous Integration (via Bitrise), Code Coverage (via Codecov), and Code Nitpicking Quality (via Codacy) have been implemented
  • The Notes folder contains some additional information on Localisation and the component parts of the air quality measurement

PSI and PM2.5 Data

There are two APIs that I’m using in Singapore Breathe:

  • PSI: This is updated hourly and contains PM10, PM2.5 (24-hour and sub-index), O3, CO, NO2, and SO2 values, with the exception of the PM2.5 hourly readings.
  • PM2.5: This contains the hourly PM2.5 readings only.

The only difference in structure between the two API responses is that PSI API returns readings for six regions (North, East, South, West, Central, and National), while the PM2.5 API returns results for five (it excludes National).

  // PSI
  "items": [
    "timestamp": "2020-12-15T08:00:00+08:00",
    "update_timestamp": "2020-12-15T08:03:50+08:00",
    "readings": {
      "o3_sub_index": {
        "west": 4,
        "national": 4,
        "east": 2,
        "central": 2,
        "south": 3,
        "north": 4
      "pm10_twenty_four_hourly": {
        "west": 20,
        "national": 25,
        "east": 25,
        "central": 21,
        "south": 24,
        "north": 19
  // PM2.5
  "items": [
    "timestamp": "2020-12-15T08:00:00+08:00",
    "update_timestamp": "2020-12-15T08:03:50+08:00",
    "readings": {
      "pm25_one_hourly": {
        "west": 5,
        "east": 3,
        "central": 1,
        "south": 5,
        "north": 2

Given the similarities, both PSI and PM2.5 responses can be modelled using the same struct.

A Combined RegionalAQM

The way I will display the readings to users is via annotations on a Map. For example, there will be an annotation for the West region which, when tapped, will show the air quality measurements for that region. As such, I’m not using the data as-is from the API. Instead, for each region, I am creating a RegionalAQM1 NSManagedObject which contains the distinct readings for that region and timestamp. In order to ensure duplicate readings are not saved to the database, the RegionalAQM has an id property that is a concatenation of region and timestamp, and the NSManagedObjectContext has a merge policy of mergeByPropertyObjectTrump.

An example RegionalAQM looks like this:

let aqm = RegionalAQM(context: PersistenceController.shared.container.viewContext)
let label = psi.regionMetadata.filter { $ == region }.first!
aqm.latitude = label.labelLocation!.latitude
aqm.longitude = label.labelLocation!.longitude
aqm.co_eight_hour_max = item.readings!["co_eight_hour_max"]!.north ?? 0.0
aqm.co_sub_index = item.readings!["co_sub_index"]!.north ?? 0.0
aqm.no2_one_hour_max = item.readings!["no2_one_hour_max"]!.north ?? 0.0
aqm.o3_eight_hour_max = item.readings!["o3_eight_hour_max"]!.north ?? 0.0
aqm.o3_sub_index = item.readings!["o3_sub_index"]!.north ?? 0.0
aqm.pm10_sub_index = item.readings!["pm10_sub_index"]!.north ?? 0.0
aqm.pm10_twenty_four_hourly = item.readings!["pm10_twenty_four_hourly"]!.north ?? 0.0
aqm.pm25_one_hourly = pm25.items.first!.readings!["pm25_one_hourly"]?.north ?? 0.0
aqm.pm25_sub_index = item.readings!["pm25_sub_index"]!.north ?? 0.0
aqm.pm25_twenty_four_hourly = item.readings!["pm25_twenty_four_hourly"]!.north ?? 0.0
aqm.psi_twenty_four_hourly = item.readings!["psi_twenty_four_hourly"]!.north ?? 0.0
aqm.so2_sub_index = item.readings!["so2_sub_index"]!.north ?? 0.0
aqm.so2_twenty_four_hourly = item.readings!["so2_twenty_four_hourly"]!.north ?? 0.0

Once the latest readings are converted to RegionalAQMs, they are saved, and the Map view’s @FetchRequest updates the map with the latest data via annotations. (I’ve yet to decide what I’ll do with the historical readings or what the retention policy will be.)


Viewing additional data related to the PSI reading can be accomplished by tapping on the annotations. When tapped a modal sheet is presented with all six data points and explanations of each reading. You can tap the (?) button2 to hide the explanations3.

Up Next

I am going to read-up a little bit more on using Core Data with SwiftUI make any necessary changes to the PersistenceController code. I also intend to add a Dock style menu to the bottom of the map with a refresh button, settings, and latest readings timestamp.

  1. AQM: Air Quality Measurement ↩︎

  2. This provides a nice example of how the @AppStorage property wrapper works. ↩︎

  3. Some definitions come from Singapore’s National Environment Agency, while others come from the U.S.A’s Environmental Protection Agency. ↩︎