fix(ios): resolve compilation errors and enable successful build
Fixed critical compilation errors preventing iOS plugin build: - Updated logger API calls from logger.debug(TAG, msg) to logger.log(.debug, msg) across all iOS plugin files to match DailyNotificationLogger interface - Fixed async/await concurrency in makeConditionalRequest using semaphore pattern - Fixed NotificationContent immutability by creating new instances instead of mutation - Changed private access control to internal for extension-accessible methods - Added iOS 15.0+ availability checks for interruptionLevel property - Fixed static member references using Self.MEMBER_NAME syntax - Added missing .scheduling case to exhaustive switch statement - Fixed variable initialization in retry state closures Added DailyNotificationStorage.swift implementation matching Android pattern. Updated build scripts with improved error reporting and full log visibility. iOS plugin now compiles successfully. All build errors resolved.
This commit is contained in:
412
ios/Plugin/DailyNotificationStorage.swift
Normal file
412
ios/Plugin/DailyNotificationStorage.swift
Normal file
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* DailyNotificationStorage.swift
|
||||
*
|
||||
* Storage management for notification content and settings
|
||||
* Implements tiered storage: Key-Value (quick) + DB (structured) + Files (large assets)
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* 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: In-memory cache 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 MAX_CACHE_SIZE = 100 // Maximum notifications to keep in memory
|
||||
private static let CACHE_CLEANUP_INTERVAL: TimeInterval = 24 * 60 * 60 // 24 hours
|
||||
private static let MAX_STORAGE_ENTRIES = 100 // Maximum total storage entries
|
||||
private static let RETENTION_PERIOD_MS: TimeInterval = 14 * 24 * 60 * 60 * 1000 // 14 days
|
||||
private static let BATCH_CLEANUP_SIZE = 50 // Clean up in batches
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let userDefaults: UserDefaults
|
||||
private var notificationCache: [String: NotificationContent] = [:]
|
||||
private var notificationList: [NotificationContent] = []
|
||||
private let storageQueue = DispatchQueue(label: "storage.queue", attributes: .concurrent)
|
||||
private let logger: DailyNotificationLogger?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param logger Optional logger instance for debugging
|
||||
*/
|
||||
init(logger: DailyNotificationLogger? = nil) {
|
||||
self.userDefaults = UserDefaults(suiteName: Self.PREFS_NAME) ?? UserDefaults.standard
|
||||
self.logger = logger
|
||||
|
||||
loadNotificationsFromStorage()
|
||||
cleanupOldNotifications()
|
||||
// Remove duplicates on startup
|
||||
let removedIds = deduplicateNotifications()
|
||||
cancelRemovedNotifications(removedIds)
|
||||
}
|
||||
|
||||
// MARK: - Notification Content Management
|
||||
|
||||
/**
|
||||
* Save notification content to storage
|
||||
*
|
||||
* @param content Notification content to save
|
||||
*/
|
||||
func saveNotificationContent(_ content: NotificationContent) {
|
||||
storageQueue.async(flags: .barrier) {
|
||||
self.logger?.log(.debug, "DN|STORAGE_SAVE_START id=\(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 }
|
||||
|
||||
// Apply storage cap and retention policy
|
||||
self.enforceStorageLimits()
|
||||
|
||||
// Persist to UserDefaults
|
||||
self.saveNotificationsToStorage()
|
||||
|
||||
self.logger?.log(.debug, "DN|STORAGE_SAVE_OK id=\(content.id) total=\(self.notificationList.count)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification content by ID
|
||||
*
|
||||
* @param id Notification ID
|
||||
* @return Notification content or nil if not found
|
||||
*/
|
||||
func getNotificationContent(_ id: String) -> NotificationContent? {
|
||||
return storageQueue.sync {
|
||||
return notificationCache[id]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last notification that was delivered
|
||||
*
|
||||
* @return Last notification or nil if none exists
|
||||
*/
|
||||
func getLastNotification() -> NotificationContent? {
|
||||
return storageQueue.sync {
|
||||
if notificationList.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the most recent delivered notification
|
||||
let currentTime = Date().timeIntervalSince1970 * 1000
|
||||
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 storageQueue.sync {
|
||||
return Array(notificationList)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notifications that are ready to be displayed
|
||||
*
|
||||
* @return Array of ready notifications
|
||||
*/
|
||||
func getReadyNotifications() -> [NotificationContent] {
|
||||
return storageQueue.sync {
|
||||
let currentTime = Date().timeIntervalSince1970 * 1000
|
||||
return notificationList.filter { $0.scheduledTime <= currentTime }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next scheduled notification
|
||||
*
|
||||
* @return Next notification or nil if none scheduled
|
||||
*/
|
||||
func getNextNotification() -> NotificationContent? {
|
||||
return storageQueue.sync {
|
||||
let currentTime = Date().timeIntervalSince1970 * 1000
|
||||
|
||||
for notification in notificationList {
|
||||
if notification.scheduledTime > currentTime {
|
||||
return notification
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove notification by ID
|
||||
*
|
||||
* @param id Notification ID to remove
|
||||
*/
|
||||
func removeNotification(_ id: String) {
|
||||
storageQueue.async(flags: .barrier) {
|
||||
self.notificationCache.removeValue(forKey: id)
|
||||
self.notificationList.removeAll { $0.id == id }
|
||||
self.saveNotificationsToStorage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all notifications
|
||||
*/
|
||||
func clearAllNotifications() {
|
||||
storageQueue.async(flags: .barrier) {
|
||||
self.notificationCache.removeAll()
|
||||
self.notificationList.removeAll()
|
||||
self.saveNotificationsToStorage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification count
|
||||
*
|
||||
* @return Number of notifications stored
|
||||
*/
|
||||
func getNotificationCount() -> Int {
|
||||
return storageQueue.sync {
|
||||
return notificationList.count
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if storage is empty
|
||||
*
|
||||
* @return true if no notifications stored
|
||||
*/
|
||||
func isEmpty() -> Bool {
|
||||
return storageQueue.sync {
|
||||
return notificationList.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Settings Management
|
||||
|
||||
/**
|
||||
* Set sound enabled setting
|
||||
*
|
||||
* @param enabled Whether sound is enabled
|
||||
*/
|
||||
func setSoundEnabled(_ enabled: Bool) {
|
||||
userDefaults.set(enabled, forKey: "sound_enabled")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sound is enabled
|
||||
*
|
||||
* @return true if sound is enabled
|
||||
*/
|
||||
func isSoundEnabled() -> Bool {
|
||||
return userDefaults.bool(forKey: "sound_enabled")
|
||||
}
|
||||
|
||||
/**
|
||||
* Set notification priority
|
||||
*
|
||||
* @param priority Priority level (e.g., "high", "normal", "low")
|
||||
*/
|
||||
func setPriority(_ priority: String) {
|
||||
userDefaults.set(priority, forKey: "priority")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification priority
|
||||
*
|
||||
* @return Priority level or "normal" if not set
|
||||
*/
|
||||
func getPriority() -> String {
|
||||
return userDefaults.string(forKey: "priority") ?? "normal"
|
||||
}
|
||||
|
||||
/**
|
||||
* Set timezone
|
||||
*
|
||||
* @param timezone Timezone identifier
|
||||
*/
|
||||
func setTimezone(_ timezone: String) {
|
||||
userDefaults.set(timezone, forKey: "timezone")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone
|
||||
*
|
||||
* @return Timezone identifier or system default
|
||||
*/
|
||||
func getTimezone() -> String {
|
||||
return userDefaults.string(forKey: "timezone") ?? TimeZone.current.identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Set adaptive scheduling enabled
|
||||
*
|
||||
* @param enabled Whether adaptive scheduling is enabled
|
||||
*/
|
||||
func setAdaptiveSchedulingEnabled(_ enabled: Bool) {
|
||||
userDefaults.set(enabled, forKey: Self.KEY_ADAPTIVE_SCHEDULING)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if adaptive scheduling is enabled
|
||||
*
|
||||
* @return true if adaptive scheduling is enabled
|
||||
*/
|
||||
func isAdaptiveSchedulingEnabled() -> Bool {
|
||||
return userDefaults.bool(forKey: Self.KEY_ADAPTIVE_SCHEDULING)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last fetch time
|
||||
*
|
||||
* @param time Last fetch time in milliseconds since epoch
|
||||
*/
|
||||
func setLastFetchTime(_ time: TimeInterval) {
|
||||
userDefaults.set(time, forKey: Self.KEY_LAST_FETCH)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last fetch time
|
||||
*
|
||||
* @return Last fetch time in milliseconds since epoch, or 0 if not set
|
||||
*/
|
||||
func getLastFetchTime() -> TimeInterval {
|
||||
return userDefaults.double(forKey: Self.KEY_LAST_FETCH)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we should fetch new content
|
||||
*
|
||||
* @param minInterval Minimum interval between fetches in milliseconds
|
||||
* @return true if enough time has passed since last fetch
|
||||
*/
|
||||
func shouldFetchNewContent(minInterval: TimeInterval) -> Bool {
|
||||
let lastFetch = getLastFetchTime()
|
||||
if lastFetch == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
let currentTime = Date().timeIntervalSince1970 * 1000
|
||||
return (currentTime - lastFetch) >= minInterval
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
/**
|
||||
* Load notifications from UserDefaults
|
||||
*/
|
||||
private func loadNotificationsFromStorage() {
|
||||
guard let data = userDefaults.data(forKey: Self.KEY_NOTIFICATIONS),
|
||||
let jsonArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
|
||||
return
|
||||
}
|
||||
|
||||
notificationList = jsonArray.compactMap { NotificationContent.fromDictionary($0) }
|
||||
notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notifications to UserDefaults
|
||||
*/
|
||||
private func saveNotificationsToStorage() {
|
||||
let jsonArray = notificationList.map { $0.toDictionary() }
|
||||
|
||||
if let data = try? JSONSerialization.data(withJSONObject: jsonArray) {
|
||||
userDefaults.set(data, forKey: Self.KEY_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old notifications based on retention policy
|
||||
*/
|
||||
private func cleanupOldNotifications() {
|
||||
let currentTime = Date().timeIntervalSince1970 * 1000
|
||||
let cutoffTime = currentTime - Self.RETENTION_PERIOD_MS
|
||||
|
||||
notificationList.removeAll { notification in
|
||||
let age = currentTime - notification.scheduledTime
|
||||
return age > Self.RETENTION_PERIOD_MS
|
||||
}
|
||||
|
||||
// Update cache
|
||||
notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce storage limits
|
||||
*/
|
||||
private func enforceStorageLimits() {
|
||||
// Remove oldest notifications if over limit
|
||||
while notificationList.count > Self.MAX_STORAGE_ENTRIES {
|
||||
let oldest = notificationList.removeFirst()
|
||||
notificationCache.removeValue(forKey: oldest.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate notifications
|
||||
*
|
||||
* @return Array of removed notification IDs
|
||||
*/
|
||||
private func deduplicateNotifications() -> [String] {
|
||||
var seen = Set<String>()
|
||||
var removed: [String] = []
|
||||
|
||||
notificationList = notificationList.filter { notification in
|
||||
if seen.contains(notification.id) {
|
||||
removed.append(notification.id)
|
||||
return false
|
||||
}
|
||||
seen.insert(notification.id)
|
||||
return true
|
||||
}
|
||||
|
||||
// Update cache
|
||||
notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) })
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel removed notifications
|
||||
*
|
||||
* @param ids Array of notification IDs to cancel
|
||||
*/
|
||||
private func cancelRemovedNotifications(_ ids: [String]) {
|
||||
// This would typically cancel alarms/workers for these IDs
|
||||
// Implementation depends on scheduler integration
|
||||
logger?.log(.debug, "DN|STORAGE_DEDUP removed=\(ids.count)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user