Files
daily-notification-plugin/ios/Plugin/DailyNotificationStorage.swift
Server 5844b92e18 feat(ios): implement Phase 1 permission methods and fix build issues
Implement checkPermissionStatus() and requestNotificationPermissions()
methods for iOS plugin, matching Android functionality. Fix compilation
errors across plugin files and add comprehensive build/test infrastructure.

Key Changes:
- Add checkPermissionStatus() and requestNotificationPermissions() methods
- Fix 13+ categories of Swift compilation errors (type conversions, logger
  API, access control, async/await, etc.)
- Create DailyNotificationScheduler, DailyNotificationStorage,
  DailyNotificationStateActor, and DailyNotificationErrorCodes components
- Fix CoreData initialization to handle missing model gracefully for Phase 1
- Add iOS test app build script with simulator auto-detection
- Update directive with lessons learned from build and permission work

Build Status:  BUILD SUCCEEDED
Test App:  Ready for iOS Simulator testing

Files Modified:
- doc/directives/0003-iOS-Android-Parity-Directive.md (lessons learned)
- ios/Plugin/DailyNotificationPlugin.swift (Phase 1 methods)
- ios/Plugin/DailyNotificationModel.swift (CoreData fix)
- 11+ other plugin files (compilation fixes)

Files Added:
- ios/Plugin/DailyNotificationScheduler.swift
- ios/Plugin/DailyNotificationStorage.swift
- ios/Plugin/DailyNotificationStateActor.swift
- ios/Plugin/DailyNotificationErrorCodes.swift
- scripts/build-ios-test-app.sh
- scripts/setup-ios-test-app.sh
- test-apps/ios-test-app/ (full test app)
- Multiple Phase 1 documentation files
2025-11-13 05:14:24 -08:00

334 lines
11 KiB
Swift

/**
* DailyNotificationStorage.swift
*
* Storage management for notification content and settings
* Implements tiered storage: UserDefaults (quick) + CoreData (structured)
*
* @author Matthew Raymer
* @version 1.0.0
*/
import Foundation
import CoreData
/**
* Manages storage for notification content and settings
*
* This class implements the tiered storage approach:
* - Tier 1: UserDefaults for quick access to settings and recent data
* - Tier 2: CoreData for structured notification content
* - Tier 3: File system for large assets (future use)
*/
class DailyNotificationStorage {
// MARK: - Constants
private static let TAG = "DailyNotificationStorage"
private static let PREFS_NAME = "DailyNotificationPrefs"
private static let KEY_NOTIFICATIONS = "notifications"
private static let KEY_SETTINGS = "settings"
private static let KEY_LAST_FETCH = "last_fetch"
private static let KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling"
private static let KEY_LAST_SUCCESSFUL_RUN = "last_successful_run"
private static let KEY_BGTASK_EARLIEST_BEGIN = "bgtask_earliest_begin"
private static let MAX_CACHE_SIZE = 100 // Maximum notifications to keep
private static let CACHE_CLEANUP_INTERVAL: TimeInterval = 24 * 60 * 60 // 24 hours
// MARK: - Properties
private let userDefaults: UserDefaults
private let database: DailyNotificationDatabase
private var notificationCache: [String: NotificationContent] = [:]
private var notificationList: [NotificationContent] = []
private let cacheQueue = DispatchQueue(label: "com.timesafari.dailynotification.storage.cache", attributes: .concurrent)
// MARK: - Initialization
/**
* Initialize storage with database path
*
* @param databasePath Path to SQLite database
*/
init(databasePath: String? = nil) {
self.userDefaults = UserDefaults.standard
let path = databasePath ?? Self.getDefaultDatabasePath()
self.database = DailyNotificationDatabase(path: path)
loadNotificationsFromStorage()
cleanupOldNotifications()
}
/**
* Get default database path
*/
private static func getDefaultDatabasePath() -> String {
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return documentsPath.appendingPathComponent("daily_notifications.db").path
}
/**
* Get current database path
*
* @return Database path
*/
func getDatabasePath() -> String {
return database.getPath()
}
// MARK: - Notification Content Management
/**
* Save notification content to storage
*
* @param content Notification content to save
*/
func saveNotificationContent(_ content: NotificationContent) {
cacheQueue.async(flags: .barrier) {
print("\(Self.TAG): Saving notification: \(content.id)")
// Add to cache
self.notificationCache[content.id] = content
// Add to list and sort by scheduled time
self.notificationList.removeAll { $0.id == content.id }
self.notificationList.append(content)
self.notificationList.sort { $0.scheduledTime < $1.scheduledTime }
// Persist to UserDefaults
self.saveNotificationsToStorage()
// Persist to CoreData
self.database.saveNotificationContent(content)
print("\(Self.TAG): Notification saved successfully")
}
}
/**
* Get notification content by ID
*
* @param id Notification ID
* @return Notification content or nil if not found
*/
func getNotificationContent(id: String) -> NotificationContent? {
return cacheQueue.sync {
return notificationCache[id]
}
}
/**
* Get the last notification that was delivered
*
* @return Last notification or nil if none exists
*/
func getLastNotification() -> NotificationContent? {
return cacheQueue.sync {
if notificationList.isEmpty {
return nil
}
// Find the most recent delivered notification
let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // milliseconds
for notification in notificationList.reversed() {
if notification.scheduledTime <= currentTime {
return notification
}
}
return nil
}
}
/**
* Get all notifications
*
* @return Array of all notifications
*/
func getAllNotifications() -> [NotificationContent] {
return cacheQueue.sync {
return Array(notificationList)
}
}
/**
* Get notifications that are ready to be displayed
*
* @return Array of ready notifications
*/
func getReadyNotifications() -> [NotificationContent] {
return cacheQueue.sync {
let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // milliseconds
return notificationList.filter { $0.scheduledTime <= currentTime }
}
}
/**
* Delete notification content by ID
*
* @param id Notification ID
*/
func deleteNotificationContent(id: String) {
cacheQueue.async(flags: .barrier) {
print("\(Self.TAG): Deleting notification: \(id)")
self.notificationCache.removeValue(forKey: id)
self.notificationList.removeAll { $0.id == id }
self.saveNotificationsToStorage()
self.database.deleteNotificationContent(id: id)
print("\(Self.TAG): Notification deleted successfully")
}
}
/**
* Clear all notification content
*/
func clearAllNotifications() {
cacheQueue.async(flags: .barrier) {
print("\(Self.TAG): Clearing all notifications")
self.notificationCache.removeAll()
self.notificationList.removeAll()
self.userDefaults.removeObject(forKey: Self.KEY_NOTIFICATIONS)
self.database.clearAllNotifications()
print("\(Self.TAG): All notifications cleared")
}
}
// MARK: - Settings Management
/**
* Save settings
*
* @param settings Settings dictionary
*/
func saveSettings(_ settings: [String: Any]) {
if let data = try? JSONSerialization.data(withJSONObject: settings) {
userDefaults.set(data, forKey: Self.KEY_SETTINGS)
print("\(Self.TAG): Settings saved")
}
}
/**
* Get settings
*
* @return Settings dictionary or empty dictionary
*/
func getSettings() -> [String: Any] {
guard let data = userDefaults.data(forKey: Self.KEY_SETTINGS),
let settings = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return [:]
}
return settings
}
// MARK: - Background Task Tracking
/**
* Save last successful BGTask run timestamp
*
* @param timestamp Timestamp in milliseconds
*/
func saveLastSuccessfulRun(timestamp: Int64) {
userDefaults.set(timestamp, forKey: Self.KEY_LAST_SUCCESSFUL_RUN)
print("\(Self.TAG): Last successful run saved: \(timestamp)")
}
/**
* Get last successful BGTask run timestamp
*
* @return Timestamp in milliseconds or nil
*/
func getLastSuccessfulRun() -> Int64? {
let timestamp = userDefaults.object(forKey: Self.KEY_LAST_SUCCESSFUL_RUN) as? Int64
return timestamp
}
/**
* Save BGTask earliest begin date
*
* @param timestamp Timestamp in milliseconds
*/
func saveBGTaskEarliestBegin(timestamp: Int64) {
userDefaults.set(timestamp, forKey: Self.KEY_BGTASK_EARLIEST_BEGIN)
print("\(Self.TAG): BGTask earliest begin saved: \(timestamp)")
}
/**
* Get BGTask earliest begin date
*
* @return Timestamp in milliseconds or nil
*/
func getBGTaskEarliestBegin() -> Int64? {
let timestamp = userDefaults.object(forKey: Self.KEY_BGTASK_EARLIEST_BEGIN) as? Int64
return timestamp
}
// MARK: - Private Helper Methods
/**
* Load notifications from UserDefaults
*/
private func loadNotificationsFromStorage() {
guard let data = userDefaults.data(forKey: Self.KEY_NOTIFICATIONS),
let notifications = try? JSONDecoder().decode([NotificationContent].self, from: data) else {
print("\(Self.TAG): No notifications found in storage")
return
}
cacheQueue.async(flags: .barrier) {
self.notificationList = notifications
for notification in notifications {
self.notificationCache[notification.id] = notification
}
print("\(Self.TAG): Loaded \(notifications.count) notifications from storage")
}
}
/**
* Save notifications to UserDefaults
*/
private func saveNotificationsToStorage() {
guard let data = try? JSONEncoder().encode(notificationList) else {
print("\(Self.TAG): Failed to encode notifications")
return
}
userDefaults.set(data, forKey: Self.KEY_NOTIFICATIONS)
}
/**
* Cleanup old notifications
*/
private func cleanupOldNotifications() {
cacheQueue.async(flags: .barrier) {
let currentTime = Int64(Date().timeIntervalSince1970 * 1000) // milliseconds
let cutoffTime = currentTime - Int64(Self.CACHE_CLEANUP_INTERVAL * 1000)
self.notificationList.removeAll { notification in
let isOld = notification.scheduledTime < cutoffTime
if isOld {
self.notificationCache.removeValue(forKey: notification.id)
}
return isOld
}
// Limit cache size
if self.notificationList.count > Self.MAX_CACHE_SIZE {
let excess = self.notificationList.count - Self.MAX_CACHE_SIZE
for i in 0..<excess {
let notification = self.notificationList[i]
self.notificationCache.removeValue(forKey: notification.id)
}
self.notificationList.removeFirst(excess)
}
self.saveNotificationsToStorage()
}
}
}