Skip to content

Commit

Permalink
UserActivity
Browse files Browse the repository at this point in the history
  • Loading branch information
kasianov-mikhail committed Jan 20, 2025
1 parent 51e2e43 commit 0531400
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 2 deletions.
10 changes: 10 additions & 0 deletions Sources/Scout/Core/IDs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,13 @@ extension EventModel {
setPrimitiveValue(IDs.launch, forKey: #keyPath(EventModel.launchID))
}
}

extension UserActivity {
public override func awakeFromInsert() {
super.awakeFromInsert()
setPrimitiveValue(UUID(), forKey: #keyPath(UserActivity.userActivityID))
setPrimitiveValue(IDs.session, forKey: #keyPath(UserActivity.sessionID))
setPrimitiveValue(IDs.user, forKey: #keyPath(UserActivity.userID))
setPrimitiveValue(IDs.launch, forKey: #keyPath(UserActivity.launchID))
}
}
4 changes: 2 additions & 2 deletions Sources/Scout/Core/NotificationListener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ public class NotificationListener {
@MainActor public static let activity = NotificationListener(table: [
UIApplication.willEnterForegroundNotification: {
try await persistentContainer.performBackgroundTask(Session.trigger)
try await persistentContainer.performBackgroundTask(ActiveUser.trigger)
try await persistentContainer.performBackgroundTask(UserActivity.trigger)
try await sync(in: container)
},
UIApplication.didEnterBackgroundNotification: {
try await persistentContainer.performBackgroundTask(Session.complete)
try await persistentContainer.performBackgroundTask(ActiveUser.trigger)
try await persistentContainer.performBackgroundTask(UserActivity.trigger)
try await sync(in: container)
},
])
Expand Down
136 changes: 136 additions & 0 deletions Sources/Scout/Core/UserActivity+Monitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// Copyright 2025 Mikhail Kasianov
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import CoreData

extension UserActivity {

/// Triggers the creation of `UserActivity` records for the current date.
///
/// This method checks if `UserActivity` records already exist for the current date.
/// If no records exist, it creates new `UserActivity` records and saves them to the context.
///
/// - Parameter context: The managed object context in which to perform the operation.
/// - Throws: An error if the operation fails.
///
static func trigger(in context: NSManagedObjectContext) throws {
try trigger(date: Date(), in: context)
}

/// Triggers the creation of `UserActivity` records for the specified date.
///
/// This method checks if `UserActivity` records already exist for the specified date.
/// If no records exist, it creates new `UserActivity` records and saves them to the context.
///
/// - Parameters:
/// - date: The date for which to create the `UserActivity` records.
/// - context: The managed object context in which to perform the operation.
/// - Throws: An error if the operation fails.
///
static func trigger(date: Date, in context: NSManagedObjectContext) throws {
let activities = try activities(for: date, in: context)

for period in ActivityPeriod.allCases {
let limit = date.startOfDay.adding(period.rangeComponent)

for activity in activities {
if let day = activity.day, day < limit {
activity[keyPath: period.countField] = 1
}
}
}

try context.save()
}
}

// MARK: - Private Functions

/// Retrieves or creates user activities for a given date within a specified context.
///
/// This function first determines the range of dates for the given date, starting from the
/// beginning of the day and extending to the end of the month. It then attempts to fetch existing
/// activities within this range from the provided context. If there are gaps in the activities,
/// it creates new activities to fill those gaps.
///
/// - Parameters:
/// - date: The date for which to retrieve or create activities.
/// - context: The managed object context used to fetch or create activities.
///
/// - Returns: An array of `UserActivity` objects for the specified date range.
///
/// - Throws: An error if there is an issue fetching existing activities from the context.
///
private func activities(for date: Date, in context: NSManagedObjectContext) throws -> [UserActivity]
{
let range = date.startOfDay..<date.startOfDay.addingMonth()

var activities = try existing(for: range, in: context)
var recent = activities.last?.day?.addingDay() ?? date.startOfDay

while recent < range.upperBound {
let activity = newActivity(date: recent, in: context)
activities.append(activity)
recent = recent.addingDay()
}

return activities
}

private func existing(for range: Range<Date>, in context: NSManagedObjectContext) throws
-> [UserActivity]
{
let request = UserActivity.fetchRequest()

request.sortDescriptors = [
NSSortDescriptor(
keyPath: \UserActivity.day,
ascending: true
)
]

request.predicate = NSPredicate(
format: "day >= %@ AND day < %@",
range.lowerBound as NSDate,
range.upperBound as NSDate
)

return try context.fetch(request)
}

private func newActivity(date: Date, in context: NSManagedObjectContext) -> UserActivity {
let entity = NSEntityDescription.entity(forEntityName: "UserActivity", in: context)!
let activeUser = UserActivity(entity: entity, insertInto: context)

activeUser.date = date
activeUser.day = date.startOfDay
activeUser.week = date.startOfWeek
activeUser.month = date.startOfMonth

return activeUser
}

// MARK: - Count Field

extension ActivityPeriod {

/// A computed property that returns the appropriate count field key path for the activity period.
///
/// This property provides the key path to the count field (`dayCount`, `weekCount`, or `monthCount`)
/// based on the activity period (`daily`, `weekly`, or `monthly`).
///
fileprivate var countField: ReferenceWritableKeyPath<UserActivity, Int32> {
switch self {
case .daily:
return \.dayCount
case .weekly:
return \.weekCount
case .monthly:
return \.monthCount
}
}
}
14 changes: 14 additions & 0 deletions Sources/Scout/Scout.xcdatamodeld/Scout.xcdatamodel/contents
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,18 @@
<attribute name="userID" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="week" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="UserActivity" representedClassName="UserActivity" syncable="YES" codeGenerationType="class">
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="day" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="dayCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isSynced" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="launchID" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="month" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="monthCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sessionID" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="userActivityID" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="userID" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="week" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="weekCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
</entity>
</model>

0 comments on commit 0531400

Please sign in to comment.