Eye Damage
/// Key technologies 
ActivityKit / MVVM / UserNotifications / PostgreSQL / Structured Concurrency /
SwiftUI / UIBezierPath / Vapor / Vapor_Queues / XCTest

Release

I published the app to the App Store!🎉
App Store Links

Introduction

Backgorund

There are various apps to discourage smartphone use. Some of them discourage smartphone use by giving rewards to users such as character items, based on the amount of time they do not use their phones. But I have had the experience of that backfired because I was looking forward to the rewards. I thought, for some people including me, the inclusion of game elements may in fact cause people rather to be distracted by their phones. And I wondered, instead of rewarding people, If I gave them a punishment based on use time, people would refrain from using their phones.

Solution

In this app, I feature a creepy eye on lock screen, blood vessels of which bleed according to smartphone usage during bedtime. With Live Activities that was introduced in iOS 16, the application can reflect smartphone usage to the eye icon in real time manner. In addition, I implemented summary report functionality so that users can identify trends about their smartphone usage time. This will help them to understand when they use their phones too much, how tired their eyes feel when they use them too much, etc.

Features

  • LocalNotifications
  • PushNotifications
  • LiveActivities
  • Local data persistence (Store daily usage reports with FileMnager)

Requirements

  • Swift: version 5.6
  • iOS: 16.1 or later
  • Vapor: version 4.0

Repository

EyeDamage Repository

Usage

  1. You will set bed time and wakeup time on the Setting tab.
  1. Once you set the time, you will receive notification at the bedtime.

  2. When you tap the notification, time recording starts. After the moment, every time you unlock smartphone (and open the application), the app records the time you start using smartphone.

  3. Before you stop using smartphone, you will tap sleep button, which will records the timing you will go to sleep.

  4. At the same timing as 3., Live Activity is triggered and you will see an eye on the lock screen, which will bloodshoot as you spend time using smartphone.

Takeaways

Problem

With default specification, Live Activity is terminated 8 hours after it is triggered. (see Apple documentation).
Its state properties are thrown away as well. With this behaviour, when you get up in the morning, you will see an icon saying "you spent 0 min 0 sec" without some devices like the image below.

Solution for the problem

I solved this problem using silent remote notification and Vapor/Queues.
The idea is to send a signal 5 minutes before Live Activity is forced to terminated, and thereby retain the eye icon state for next morning.
While user is sleeping, application will receive a silent notification in the background and use a method to terminate Live Activity in a notification delegate method. This will prevent the lock screen from being reset the next morning.

I used following technologies to achieve this.

  • Vapor/Queues (to schedule a futer task)
  • Silent Remote Notification (notify an application something from server side when user application is not in foreground)

1. Schedule future tasks

With Vapor/Queues, you can set a task regulary or at specific timing.

  • Job Tasks are defined by the Job or AsyncJob protocol.
struct ActivityInfo: Codable {
  let pushToken: String
}

struct BackgroundLiveActivitiesUpdateJob: AsyncJob {
  typealias Payload = ActivityInfo
  
  func dequeue(_ context: Queues.QueueContext, _ payload: ActivityInfo) async throws {
    // this method will send silent remote notification to a client
    try await APNSManager.sendBackgroundNotification(token: payload.pushToken, client: APNSManager.getClient(), activityInfo: payload)
  }
}
  • Registering a Job After modeling a job you must add it to your configuration section like this:
// in configure method, register Job
let bgLiveActivityJob = BackgroundLiveActivitiesUpdateJob()
app.queues.add(bgLiveActivityJob)
  • Running Workers in-process To run a worker in the same process as your application (as opposed to starting a whole separate server to handle it), call the convenience methods on Application:
// in configure method, run worker
try app.queues.startInProcessJobs(on: .default)
  • Dispatching Jobs To dispatch a queue job, you need access to an instance of Application or Request.
    You will most likely be dispatching jobs inside of a route handler:
    With delayUntil argument, you can specify time to trigger the job
// inside route handler function
try await req.queue.dispatch(
  BackgroundLiveActivitiesUpdateJob.self,
  activityInfo,
  delayUntil: dispatchDate
)

2. Send Silent Remote Notification

You can receive remote notification using

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult 

Inside the delegate, I ended the ongoing Live Activity.
This will keep the eye icon up-to-date even 8 hours after its start.

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult {
    await Activities.terminateActivity()
    return .noData // always return no data (regardless termination succeeds or not)
}