diff --git a/CHANGELOG.md b/CHANGELOG.md
index fdac0dd84..371425f93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Decreased the opacity on disabled buttons.
### Internal Changes
+- Moved the database cleanup routine into a background execution task. [#1426](https://github.com/planetary-social/nos/issues/1426)
- Fix the Crowdin GitHub integration by using the official GitHub action. [#1520](https://github.com/planetary-social/nos/issues/1520)
- Update Xcode to version 15.4, adding compatibility for Xcode 16.
- Reduced spammy "Failed to parse Follow" log messages.
diff --git a/Nos.xcodeproj/project.pbxproj b/Nos.xcodeproj/project.pbxproj
index 8a18135be..2a00fc501 100644
--- a/Nos.xcodeproj/project.pbxproj
+++ b/Nos.xcodeproj/project.pbxproj
@@ -2863,8 +2863,9 @@
GCC_PREPROCESSOR_DEFINITIONS = "STAGING=1";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nos/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = "Nos Staging";
+ INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
+ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -3038,6 +3039,7 @@
INFOPLIST_FILE = Nos/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
+ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -3307,7 +3309,9 @@
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nos/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
+ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -3362,7 +3366,9 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nos/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
+ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
diff --git a/Nos/Controller/PersistenceController.swift b/Nos/Controller/PersistenceController.swift
index 4a2db75a5..782efe8fc 100644
--- a/Nos/Controller/PersistenceController.swift
+++ b/Nos/Controller/PersistenceController.swift
@@ -1,6 +1,7 @@
import CoreData
import Logger
import Dependencies
+import BackgroundTasks
final class PersistenceController {
@@ -121,6 +122,14 @@ final class PersistenceController {
private static func saveVersionToDisk(_ newVersion: Int) {
UserDefaults.standard.set(newVersion, forKey: Self.versionKey)
}
+}
+
+// MARK: - Maintenance & Cleanup
+
+extension PersistenceController {
+
+ /// The ID of the background task that runs the `DatabaseCleaner`. Needs to match the value in Info.plist.
+ static let cleanupBackgroundTaskID = "com.verse.nos.database-cleaner"
/// Cleans up unneeded entities from the database. Our local database is really just a cache, and we need to
/// invalidate old items to keep it from growing indefinitely.
@@ -140,6 +149,36 @@ final class PersistenceController {
}
}
+ /// Tells iOS to wake our app up in the background to run `cleanupEntities()` periodically. This tends to run
+ /// once a day when the device is connected to power.
+ func scheduleBackgroundCleanupTask() {
+ @Dependency(\.analytics) var analytics
+ @Dependency(\.crashReporting) var crashReporting
+ let scheduler = BGTaskScheduler.shared
+
+ scheduler.register(forTaskWithIdentifier: Self.cleanupBackgroundTaskID, using: nil) { task in
+
+ task.expirationHandler = {
+ analytics.databaseCleanupTaskExpired()
+ }
+
+ Task {
+ await self.cleanupEntities()
+ task.setTaskCompleted(success: true)
+ }
+ }
+
+ let request = BGProcessingTaskRequest(identifier: Self.cleanupBackgroundTaskID)
+ request.requiresNetworkConnectivity = false
+ request.requiresExternalPower = false
+
+ do {
+ try scheduler.submit(request)
+ } catch {
+ crashReporting.report("Unable to schedule background task: \(error)")
+ }
+ }
+
static func databaseStatistics(from context: NSManagedObjectContext) async throws -> [(String, Int)] {
try await context.perform {
var statistics = [String: Int]()
@@ -160,6 +199,8 @@ final class PersistenceController {
}
}
+// MARK: - Testing & Debug
+
#if DEBUG
extension PersistenceController {
func loadSampleData(context: NSManagedObjectContext) async throws {
diff --git a/Nos/Info.plist b/Nos/Info.plist
index bfdbc42d9..7815a6e91 100644
--- a/Nos/Info.plist
+++ b/Nos/Info.plist
@@ -46,6 +46,7 @@
UIBackgroundModes
remote-notification
+ processing
UNS_API_KEY
$(UNS_API_KEY)
@@ -63,5 +64,9 @@
Nos uses the microphone when you are record a video from within the app.
WALLET_CONNECT_PROJECT_ID
$(WALLET_CONNECT_PROJECT_ID)
+ BGTaskSchedulerPermittedIdentifiers
+
+ com.verse.nos.database-cleaner
+
diff --git a/Nos/NosApp.swift b/Nos/NosApp.swift
index fbc27ff61..188a98189 100644
--- a/Nos/NosApp.swift
+++ b/Nos/NosApp.swift
@@ -21,6 +21,7 @@ struct NosApp: App {
// hack to fix confirmationDialog color issue
// https://github.com/planetary-social/nos/issues/1064
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = .systemBlue
+ persistenceController.scheduleBackgroundCleanupTask()
}
var body: some Scene {
@@ -33,9 +34,6 @@ struct NosApp: App {
.environment(currentUser)
.environmentObject(pushNotificationService)
.onOpenURL { DeepLinkService.handle($0, router: router) }
- .task {
- await persistenceController.cleanupEntities()
- }
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .inactive:
diff --git a/Nos/Service/Analytics.swift b/Nos/Service/Analytics.swift
index 22a520b85..282643c59 100644
--- a/Nos/Service/Analytics.swift
+++ b/Nos/Service/Analytics.swift
@@ -129,6 +129,18 @@ class Analytics {
track("Database Statistics", properties: properties)
}
+ func databaseCleanupStarted() {
+ track("Database Cleanup Started")
+ }
+
+ func databaseCleanupTaskExpired() {
+ track("Database Cleanup Task Expired")
+ }
+
+ func databaseCleanupCompleted(duration: TimeInterval) {
+ track("Database Cleanup Completed", properties: ["duration": duration])
+ }
+
func logout() {
track("Logged out")
postHog?.reset()
diff --git a/Nos/Service/DatabaseCleaner.swift b/Nos/Service/DatabaseCleaner.swift
index a695cb683..bfe3f84ff 100644
--- a/Nos/Service/DatabaseCleaner.swift
+++ b/Nos/Service/DatabaseCleaner.swift
@@ -23,6 +23,7 @@ enum DatabaseCleaner {
@Dependency(\.analytics) var analytics
let startTime = Date.now
+ analytics.databaseCleanupStarted()
Log.info("Starting Core Data cleanup...")
Log.info("Database statistics: \(try await PersistenceController.databaseStatistics(from: context))")
@@ -78,6 +79,7 @@ enum DatabaseCleaner {
let elapsedTime = Date.now.timeIntervalSince1970 - startTime.timeIntervalSince1970
Log.info("Finished Core Data cleanup in \(elapsedTime) seconds.")
+ analytics.databaseCleanupCompleted(duration: elapsedTime)
}
/// This converts old hydrated events back to stubs. We do this because EventReferences can form long chains