docs(ios): add comprehensive iOS implementation documentation
Adds complete iOS documentation suite to support iOS implementation parity with Android features. Includes implementation directives, recovery scenario mappings, database migration guide, troubleshooting guide, and test scripts. New Documentation: - iOS Implementation Directive: Phase-based implementation guide mirroring Android structure with iOS-specific considerations - iOS Recovery Scenario Mapping: Maps Android recovery scenarios to iOS equivalents with detection logic comparisons - iOS Core Data Migration Guide: Complete Room → Core Data entity mappings with implementation checklist for missing entities - iOS Troubleshooting Guide: Common issues, debugging techniques, and error code reference Enhanced Documentation: - API.md: Added iOS-only methods (permissions, background tasks, pending notifications), platform differences table, and iOS-specific error types. Updated version to 2.3.0. Test Infrastructure: - iOS test scripts for Phase 1 (cold start), Phase 2 (termination), and Phase 3 (boot recovery) testing scenarios All documentation addresses gaps identified in iOS Implementation Documentation Review and provides foundation for iOS recovery feature implementation (currently pending). Note: iOS recovery features (ReactivationManager, scenario detection) are NOT yet implemented. Documentation is ready to guide implementation.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# iOS Implementation Documentation Review
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: December 2024
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Documentation Review for iOS Implementation
|
||||
**Purpose**: Ensure Android plugin and test app documentation contains sufficient detail for iOS implementation to mirror all features
|
||||
|
||||
@@ -653,6 +653,6 @@ The Android plugin and test app documentation is **comprehensive and well-struct
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: December 2024
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After iOS implementation begins
|
||||
|
||||
|
||||
697
docs/ios-core-data-migration.md
Normal file
697
docs/ios-core-data-migration.md
Normal file
@@ -0,0 +1,697 @@
|
||||
# iOS Core Data Migration Guide: Android Room → iOS Core Data
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Database Migration Reference
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This document provides a comprehensive mapping guide for migrating Android Room database entities to iOS Core Data entities, ensuring cross-platform data consistency and feature parity.
|
||||
|
||||
**Reference**:
|
||||
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt) - Android Room schema
|
||||
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld) - iOS Core Data model
|
||||
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md) - Unified schema design
|
||||
|
||||
---
|
||||
|
||||
## 1. Entity Mapping Overview
|
||||
|
||||
### 1.1 Complete Entity Mapping
|
||||
|
||||
| Android Room Entity | iOS Core Data Entity | Status | Priority |
|
||||
| ------------------- | -------------------- | ------ | -------- |
|
||||
| `ContentCache` | `ContentCache` | ✅ Implemented | - |
|
||||
| `Schedule` | `Schedule` | ✅ Implemented | - |
|
||||
| `Callback` | `Callback` | ✅ Implemented | - |
|
||||
| `History` | `History` | ✅ Implemented | - |
|
||||
| `NotificationContentEntity` | `NotificationContent` | ❌ Missing | **High** |
|
||||
| `NotificationDeliveryEntity` | `NotificationDelivery` | ❌ Missing | **High** |
|
||||
| `NotificationConfigEntity` | `NotificationConfig` | ❌ Missing | **Medium** |
|
||||
|
||||
### 1.2 Current Implementation Status
|
||||
|
||||
**✅ Implemented (4 entities)**:
|
||||
- `ContentCache` - Fetched content with TTL
|
||||
- `Schedule` - Recurring schedule patterns
|
||||
- `Callback` - Callback configurations
|
||||
- `History` - Execution history
|
||||
|
||||
**❌ Missing (3 entities)**:
|
||||
- `NotificationContent` - Specific notification instances
|
||||
- `NotificationDelivery` - Delivery tracking/analytics
|
||||
- `NotificationConfig` - Configuration management
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Entity Mappings
|
||||
|
||||
### 2.1 ContentCache Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "content_cache")
|
||||
data class ContentCache(
|
||||
@PrimaryKey val id: String,
|
||||
val fetchedAt: Long, // epoch ms
|
||||
val ttlSeconds: Int,
|
||||
val payload: ByteArray, // BLOB
|
||||
val meta: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(ContentCache)
|
||||
public class ContentCache: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var fetchedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int32
|
||||
@NSManaged public var payload: Data?
|
||||
@NSManaged public var meta: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ `ByteArray` → `Data` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Schedule Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "schedules")
|
||||
data class Schedule(
|
||||
@PrimaryKey val id: String,
|
||||
val kind: String, // 'fetch' or 'notify'
|
||||
val cron: String? = null,
|
||||
val clockTime: String? = null, // HH:mm
|
||||
val enabled: Boolean = true,
|
||||
val lastRunAt: Long? = null, // epoch ms
|
||||
val nextRunAt: Long? = null, // epoch ms
|
||||
val jitterMs: Int = 0,
|
||||
val backoffPolicy: String = "exp",
|
||||
val stateJson: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(Schedule)
|
||||
public class Schedule: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var cron: String?
|
||||
@NSManaged public var clockTime: String?
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var lastRunAt: Date?
|
||||
@NSManaged public var nextRunAt: Date?
|
||||
@NSManaged public var jitterMs: Int32
|
||||
@NSManaged public var backoffPolicy: String?
|
||||
@NSManaged public var stateJson: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Default values preserved
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Callback Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "callbacks")
|
||||
data class Callback(
|
||||
@PrimaryKey val id: String,
|
||||
val kind: String, // 'http', 'local', 'queue'
|
||||
val target: String,
|
||||
val headersJson: String? = null,
|
||||
val enabled: Boolean = true,
|
||||
val createdAt: Long // epoch ms
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(Callback)
|
||||
public class Callback: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var target: String?
|
||||
@NSManaged public var headersJson: String?
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var createdAt: Date?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ✅ All fields mapped correctly
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Status**: ✅ **Complete**
|
||||
|
||||
---
|
||||
|
||||
### 2.4 History Entity
|
||||
|
||||
**Status**: ✅ **Implemented**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "history")
|
||||
data class History(
|
||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||
val refId: String,
|
||||
val kind: String, // fetch/notify/callback
|
||||
val occurredAt: Long, // epoch ms
|
||||
val durationMs: Long? = null,
|
||||
val outcome: String,
|
||||
val diagJson: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```swift
|
||||
@objc(History)
|
||||
public class History: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var refId: String?
|
||||
@NSManaged public var kind: String?
|
||||
@NSManaged public var occurredAt: Date?
|
||||
@NSManaged public var durationMs: Int32
|
||||
@NSManaged public var outcome: String?
|
||||
@NSManaged public var diagJson: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- ⚠️ `id` type differs: Android uses `Int` (auto-generated), iOS uses `String`
|
||||
- ✅ `Long` (epoch ms) → `Date` conversion handled
|
||||
- ✅ Optional fields properly marked
|
||||
|
||||
**Migration Consideration**: iOS uses `String` for `id` instead of auto-generated `Int`. This is acceptable as long as IDs are generated as UUIDs.
|
||||
|
||||
**Migration Status**: ✅ **Complete** (with note)
|
||||
|
||||
---
|
||||
|
||||
### 2.5 NotificationContent Entity
|
||||
|
||||
**Status**: ❌ **Missing - High Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "notification_content")
|
||||
data class NotificationContentEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val pluginVersion: String?,
|
||||
val timesafariDid: String?,
|
||||
val notificationType: String?,
|
||||
val title: String?,
|
||||
val body: String?,
|
||||
val scheduledTime: Long, // epoch ms
|
||||
val timezone: String?,
|
||||
val priority: Int,
|
||||
val vibrationEnabled: Boolean,
|
||||
val soundEnabled: Boolean,
|
||||
val mediaUrl: String?,
|
||||
val encryptedContent: String?,
|
||||
val encryptionKeyId: String?,
|
||||
val createdAt: Long, // epoch ms
|
||||
val updatedAt: Long, // epoch ms
|
||||
val ttlSeconds: Long,
|
||||
val deliveryStatus: String?,
|
||||
val deliveryAttempts: Int,
|
||||
val lastDeliveryAttempt: Long, // epoch ms
|
||||
val userInteractionCount: Int,
|
||||
val lastUserInteraction: Long, // epoch ms
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationContent)
|
||||
public class NotificationContent: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var pluginVersion: String?
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var notificationType: String?
|
||||
@NSManaged public var title: String?
|
||||
@NSManaged public var body: String?
|
||||
@NSManaged public var scheduledTime: Date?
|
||||
@NSManaged public var timezone: String?
|
||||
@NSManaged public var priority: Int32
|
||||
@NSManaged public var vibrationEnabled: Bool
|
||||
@NSManaged public var soundEnabled: Bool
|
||||
@NSManaged public var mediaUrl: String?
|
||||
@NSManaged public var encryptedContent: String?
|
||||
@NSManaged public var encryptionKeyId: String?
|
||||
@NSManaged public var createdAt: Date?
|
||||
@NSManaged public var updatedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int64
|
||||
@NSManaged public var deliveryStatus: String?
|
||||
@NSManaged public var deliveryAttempts: Int32
|
||||
@NSManaged public var lastDeliveryAttempt: Date?
|
||||
@NSManaged public var userInteractionCount: Int32
|
||||
@NSManaged public var lastUserInteraction: Date?
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Required Core Data Model XML**:
|
||||
```xml
|
||||
<entity name="NotificationContent" representedClassName="NotificationContent" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="NO" attributeType="String"/>
|
||||
<attribute name="pluginVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="timesafariDid" optional="YES" attributeType="String"/>
|
||||
<attribute name="notificationType" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="body" optional="YES" attributeType="String"/>
|
||||
<attribute name="scheduledTime" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="timezone" optional="YES" attributeType="String"/>
|
||||
<attribute name="priority" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="vibrationEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="soundEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="mediaUrl" optional="YES" attributeType="String"/>
|
||||
<attribute name="encryptedContent" optional="YES" attributeType="String"/>
|
||||
<attribute name="encryptionKeyId" optional="YES" attributeType="String"/>
|
||||
<attribute name="createdAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="updatedAt" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="ttlSeconds" optional="YES" attributeType="Integer 64" defaultValueString="604800" usesScalarValueType="YES"/>
|
||||
<attribute name="deliveryStatus" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryAttempts" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastDeliveryAttempt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userInteractionCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUserInteraction" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="String"/>
|
||||
<index name="index_notification_content_timesafari_did">
|
||||
<indexElement value="timesafariDid"/>
|
||||
</index>
|
||||
<index name="index_notification_content_notification_type">
|
||||
<indexElement value="notificationType"/>
|
||||
</index>
|
||||
<index name="index_notification_content_scheduled_time">
|
||||
<indexElement value="scheduledTime"/>
|
||||
</index>
|
||||
</entity>
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Int` → `Int32` conversion
|
||||
- `Long` (ttlSeconds) → `Int64` conversion
|
||||
- Indexes should be added for performance
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
### 2.6 NotificationDelivery Entity
|
||||
|
||||
**Status**: ❌ **Missing - High Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(
|
||||
tableName = "notification_delivery",
|
||||
foreignKeys = @ForeignKey(
|
||||
entity = NotificationContentEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["notification_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
)
|
||||
data class NotificationDeliveryEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val notificationId: String,
|
||||
val timesafariDid: String?,
|
||||
val deliveryTimestamp: Long, // epoch ms
|
||||
val deliveryStatus: String?,
|
||||
val deliveryMethod: String?,
|
||||
val deliveryAttemptNumber: Int,
|
||||
val deliveryDurationMs: Long,
|
||||
val userInteractionType: String?,
|
||||
val userInteractionTimestamp: Long, // epoch ms
|
||||
val userInteractionDurationMs: Long,
|
||||
val errorCode: String?,
|
||||
val errorMessage: String?,
|
||||
val deviceInfo: String?,
|
||||
val networkInfo: String?,
|
||||
val batteryLevel: Int,
|
||||
val dozeModeActive: Boolean,
|
||||
val exactAlarmPermission: Boolean,
|
||||
val notificationPermission: Boolean,
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationDelivery)
|
||||
public class NotificationDelivery: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var notificationId: String?
|
||||
@NSManaged public var notificationContent: NotificationContent? // Relationship
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var deliveryTimestamp: Date?
|
||||
@NSManaged public var deliveryStatus: String?
|
||||
@NSManaged public var deliveryMethod: String?
|
||||
@NSManaged public var deliveryAttemptNumber: Int32
|
||||
@NSManaged public var deliveryDurationMs: Int64
|
||||
@NSManaged public var userInteractionType: String?
|
||||
@NSManaged public var userInteractionTimestamp: Date?
|
||||
@NSManaged public var userInteractionDurationMs: Int64
|
||||
@NSManaged public var errorCode: String?
|
||||
@NSManaged public var errorMessage: String?
|
||||
@NSManaged public var deviceInfo: String?
|
||||
@NSManaged public var networkInfo: String?
|
||||
@NSManaged public var batteryLevel: Int32
|
||||
@NSManaged public var dozeModeActive: Bool
|
||||
@NSManaged public var exactAlarmPermission: Bool
|
||||
@NSManaged public var notificationPermission: Bool
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Required Core Data Model XML**:
|
||||
```xml
|
||||
<entity name="NotificationDelivery" representedClassName="NotificationDelivery" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" optional="NO" attributeType="String"/>
|
||||
<attribute name="notificationId" optional="YES" attributeType="String"/>
|
||||
<attribute name="timesafariDid" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryTimestamp" optional="NO" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="deliveryStatus" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryMethod" optional="YES" attributeType="String"/>
|
||||
<attribute name="deliveryAttemptNumber" optional="YES" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
|
||||
<attribute name="deliveryDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="userInteractionType" optional="YES" attributeType="String"/>
|
||||
<attribute name="userInteractionTimestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="userInteractionDurationMs" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="errorCode" optional="YES" attributeType="String"/>
|
||||
<attribute name="errorMessage" optional="YES" attributeType="String"/>
|
||||
<attribute name="deviceInfo" optional="YES" attributeType="String"/>
|
||||
<attribute name="networkInfo" optional="YES" attributeType="String"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||
<attribute name="dozeModeActive" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="exactAlarmPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="notificationPermission" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="String"/>
|
||||
<relationship name="notificationContent" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="NotificationContent" inverseName="deliveries" inverseEntity="NotificationContent"/>
|
||||
<index name="index_notification_delivery_notification_id">
|
||||
<indexElement value="notificationId"/>
|
||||
</index>
|
||||
<index name="index_notification_delivery_delivery_timestamp">
|
||||
<indexElement value="deliveryTimestamp"/>
|
||||
</index>
|
||||
</entity>
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- Foreign key relationship should be modeled as Core Data relationship
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Int` → `Int32` conversion
|
||||
- `Long` (duration) → `Int64` conversion
|
||||
- Cascade delete should be configured
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
### 2.7 NotificationConfig Entity
|
||||
|
||||
**Status**: ❌ **Missing - Medium Priority**
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@Entity(tableName = "notification_config")
|
||||
data class NotificationConfigEntity(
|
||||
@PrimaryKey val id: String,
|
||||
val timesafariDid: String?,
|
||||
val configType: String?,
|
||||
val configKey: String?,
|
||||
val configValue: String?,
|
||||
val configDataType: String?,
|
||||
val isEncrypted: Boolean,
|
||||
val encryptionKeyId: String?,
|
||||
val createdAt: Long, // epoch ms
|
||||
val updatedAt: Long, // epoch ms
|
||||
val ttlSeconds: Long,
|
||||
val isActive: Boolean,
|
||||
val metadata: String?
|
||||
)
|
||||
```
|
||||
|
||||
**Required iOS Core Data Entity**:
|
||||
```swift
|
||||
@objc(NotificationConfig)
|
||||
public class NotificationConfig: NSManagedObject {
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var timesafariDid: String?
|
||||
@NSManaged public var configType: String?
|
||||
@NSManaged public var configKey: String?
|
||||
@NSManaged public var configValue: String?
|
||||
@NSManaged public var configDataType: String?
|
||||
@NSManaged public var isEncrypted: Bool
|
||||
@NSManaged public var encryptionKeyId: String?
|
||||
@NSManaged public var createdAt: Date?
|
||||
@NSManaged public var updatedAt: Date?
|
||||
@NSManaged public var ttlSeconds: Int64
|
||||
@NSManaged public var isActive: Bool
|
||||
@NSManaged public var metadata: String?
|
||||
}
|
||||
```
|
||||
|
||||
**Mapping Notes**:
|
||||
- `Long` (epoch ms) → `Date` conversion required
|
||||
- `Long` (ttlSeconds) → `Int64` conversion
|
||||
- Indexes should be added for performance
|
||||
|
||||
**Migration Status**: ❌ **Not Implemented**
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Type Conversions
|
||||
|
||||
### 3.1 Time Conversions
|
||||
|
||||
**Android → iOS**:
|
||||
- `Long` (epoch milliseconds) → `Date`
|
||||
- Conversion: `Date(timeIntervalSince1970: Double(milliseconds) / 1000.0)`
|
||||
|
||||
**iOS → Android**:
|
||||
- `Date` → `Long` (epoch milliseconds)
|
||||
- Conversion: `Int64(date.timeIntervalSince1970 * 1000)`
|
||||
|
||||
### 3.2 Numeric Conversions
|
||||
|
||||
| Android Type | iOS Type | Notes |
|
||||
| ------------ | -------- | ----- |
|
||||
| `Int` | `Int32` | Direct mapping |
|
||||
| `Long` | `Int64` | For large values |
|
||||
| `Boolean` | `Bool` | Direct mapping |
|
||||
| `ByteArray` | `Data` | Binary data |
|
||||
|
||||
### 3.3 String Conversions
|
||||
|
||||
- `String?` → `String?` (direct mapping)
|
||||
- JSON fields: `String?` → `String?` (parse as needed)
|
||||
|
||||
---
|
||||
|
||||
## 4. Index Mapping
|
||||
|
||||
### 4.1 Required Indexes
|
||||
|
||||
**NotificationContent**:
|
||||
- `timesafariDid` (for user queries)
|
||||
- `notificationType` (for type filtering)
|
||||
- `scheduledTime` (for time-based queries)
|
||||
- `createdAt` (for chronological queries)
|
||||
|
||||
**NotificationDelivery**:
|
||||
- `notificationId` (for foreign key lookups)
|
||||
- `deliveryTimestamp` (for time-based queries)
|
||||
- `deliveryStatus` (for status filtering)
|
||||
- `timesafariDid` (for user queries)
|
||||
|
||||
**NotificationConfig**:
|
||||
- `timesafariDid` (for user queries)
|
||||
- `configType` (for type filtering)
|
||||
- `updatedAt` (for chronological queries)
|
||||
|
||||
---
|
||||
|
||||
## 5. Relationship Mapping
|
||||
|
||||
### 5.1 Foreign Key Relationships
|
||||
|
||||
**Android Room**:
|
||||
```kotlin
|
||||
@ForeignKey(
|
||||
entity = NotificationContentEntity::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["notification_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
```
|
||||
|
||||
**iOS Core Data**:
|
||||
```xml
|
||||
<relationship
|
||||
name="notificationContent"
|
||||
optional="YES"
|
||||
maxCount="1"
|
||||
deletionRule="Cascade"
|
||||
destinationEntity="NotificationContent"
|
||||
inverseName="deliveries"
|
||||
inverseEntity="NotificationContent"/>
|
||||
```
|
||||
|
||||
**Inverse Relationship** (NotificationContent → NotificationDelivery):
|
||||
```xml
|
||||
<relationship
|
||||
name="deliveries"
|
||||
optional="YES"
|
||||
toMany="YES"
|
||||
deletionRule="Nullify"
|
||||
destinationEntity="NotificationDelivery"
|
||||
inverseName="notificationContent"
|
||||
inverseEntity="NotificationDelivery"/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Checklist
|
||||
|
||||
### 6.1 High Priority (Required for Feature Parity)
|
||||
|
||||
- [ ] Add `NotificationContent` entity to Core Data model
|
||||
- [ ] Add `NotificationDelivery` entity to Core Data model
|
||||
- [ ] Configure foreign key relationship between `NotificationContent` and `NotificationDelivery`
|
||||
- [ ] Add required indexes for performance
|
||||
- [ ] Implement Swift extensions for entity classes
|
||||
- [ ] Add data conversion helpers (Date ↔ Long)
|
||||
- [ ] Test entity creation and relationships
|
||||
- [ ] Test cascade delete behavior
|
||||
|
||||
### 6.2 Medium Priority (Configuration Management)
|
||||
|
||||
- [ ] Add `NotificationConfig` entity to Core Data model
|
||||
- [ ] Add required indexes
|
||||
- [ ] Implement Swift extensions
|
||||
- [ ] Test configuration CRUD operations
|
||||
|
||||
### 6.3 Low Priority (Optimization)
|
||||
|
||||
- [ ] Add migration policies for schema changes
|
||||
- [ ] Add data validation rules
|
||||
- [ ] Optimize fetch requests with predicates
|
||||
- [ ] Add batch operations support
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration Steps
|
||||
|
||||
### 7.1 Step 1: Update Core Data Model
|
||||
|
||||
1. Open `DailyNotificationModel.xcdatamodeld` in Xcode
|
||||
2. Add `NotificationContent` entity with all attributes
|
||||
3. Add `NotificationDelivery` entity with all attributes
|
||||
4. Add `NotificationConfig` entity with all attributes
|
||||
5. Configure relationships between entities
|
||||
6. Add indexes for performance
|
||||
7. Set code generation to "Class Definition"
|
||||
|
||||
### 7.2 Step 2: Create Swift Extensions
|
||||
|
||||
1. Create `NotificationContent+CoreDataClass.swift`
|
||||
2. Create `NotificationContent+CoreDataProperties.swift`
|
||||
3. Create `NotificationDelivery+CoreDataClass.swift`
|
||||
4. Create `NotificationDelivery+CoreDataProperties.swift`
|
||||
5. Create `NotificationConfig+CoreDataClass.swift`
|
||||
6. Create `NotificationConfig+CoreDataProperties.swift`
|
||||
|
||||
### 7.3 Step 3: Implement Data Access Layer
|
||||
|
||||
1. Create DAO classes for each entity
|
||||
2. Implement CRUD operations
|
||||
3. Add data conversion helpers
|
||||
4. Add query methods with predicates
|
||||
|
||||
### 7.4 Step 4: Update Persistence Controller
|
||||
|
||||
1. Update `PersistenceController` to handle new entities
|
||||
2. Add migration policies if needed
|
||||
3. Test database initialization
|
||||
|
||||
### 7.5 Step 5: Testing
|
||||
|
||||
1. Test entity creation
|
||||
2. Test relationships
|
||||
3. Test cascade delete
|
||||
4. Test data conversion (Date ↔ Long)
|
||||
5. Test query performance with indexes
|
||||
|
||||
---
|
||||
|
||||
## 8. Data Migration (If Needed)
|
||||
|
||||
### 8.1 Migration from SQLite to Core Data
|
||||
|
||||
If migrating existing SQLite data to Core Data:
|
||||
|
||||
1. Read data from SQLite database
|
||||
2. Convert data types (Long → Date, etc.)
|
||||
3. Create Core Data entities
|
||||
4. Save to Core Data store
|
||||
5. Verify data integrity
|
||||
|
||||
### 8.2 Migration Script Example
|
||||
|
||||
```swift
|
||||
func migrateSQLiteToCoreData(sqlitePath: String, coreDataStack: NSPersistentContainer) {
|
||||
// 1. Open SQLite database
|
||||
// 2. Query all tables
|
||||
// 3. Convert each row to Core Data entity
|
||||
// 4. Save to Core Data store
|
||||
// 5. Verify migration success
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
- [Android Database Schema](../android/src/main/java/com/timesafari/dailynotification/DatabaseSchema.kt)
|
||||
- [iOS Core Data Model](../ios/Plugin/DailyNotificationModel.xcdatamodeld)
|
||||
- [Database Consolidation Plan](../android/DATABASE_CONSOLIDATION_PLAN.md)
|
||||
- [Core Data Programming Guide](https://developer.apple.com/documentation/coredata)
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After missing entities are implemented
|
||||
|
||||
395
docs/ios-implementation-directive.md
Normal file
395
docs/ios-implementation-directive.md
Normal file
@@ -0,0 +1,395 @@
|
||||
# iOS Implementation Directive: App Launch Recovery & Missed Notification Detection
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: Active Implementation Directive - iOS Only
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This directive provides **descriptive overview and integration guidance** for iOS-specific recovery and missed notification detection:
|
||||
|
||||
1. App Launch Recovery (cold/warm/terminated)
|
||||
2. Missed Notification Detection
|
||||
3. App Termination Detection
|
||||
4. Background Task Registration for Boot Recovery
|
||||
|
||||
**⚠️ CRITICAL**: This document is **descriptive and integrative**. The **normative implementation instructions** are in the Phase 1–3 directives below. **If any code or behavior in this file conflicts with a Phase directive, the Phase directive wins.**
|
||||
|
||||
**Reference**: See [Plugin Requirements](./alarms/03-plugin-requirements.md) for requirements that Phase directives implement.
|
||||
|
||||
**Reference**: See [Platform Capability Reference](./alarms/01-platform-capability-reference.md) for iOS OS-level facts.
|
||||
|
||||
**⚠️ IMPORTANT**: For implementation, use the phase-specific directives (these are the canonical source of truth):
|
||||
|
||||
- **[Phase 1: Cold Start Recovery](./ios-implementation-directive-phase1.md)** - Minimal viable recovery
|
||||
- Implements: [Plugin Requirements §3.1.2](./alarms/03-plugin-requirements.md#312-app-cold-start) (iOS equivalent)
|
||||
- Explicit acceptance criteria, rollback safety, data integrity checks
|
||||
- **Start here** for fastest implementation
|
||||
|
||||
- **[Phase 2: App Termination Detection & Recovery](./ios-implementation-directive-phase2.md)** - Comprehensive termination handling
|
||||
- Implements: iOS-specific app termination scenarios
|
||||
- Prerequisite: Phase 1 complete
|
||||
|
||||
- **[Phase 3: Background Task Registration & Boot Recovery](./ios-implementation-directive-phase3.md)** - Background task enhancement
|
||||
- Implements: BGTaskScheduler registration for boot recovery
|
||||
- Prerequisites: Phase 1 and Phase 2 complete
|
||||
|
||||
**See Also**: [Unified Alarm Directive](./alarms/000-UNIFIED-ALARM-DIRECTIVE.md) for master coordination document.
|
||||
|
||||
---
|
||||
|
||||
## 1. Implementation Overview
|
||||
|
||||
### 1.1 What Needs to Be Implemented
|
||||
|
||||
| Feature | Status | Priority | Location |
|
||||
| ------- | ------ | -------- | -------- |
|
||||
| App Launch Recovery | ❌ Missing | **High** | `DailyNotificationPlugin.swift` - `load()` method |
|
||||
| Missed Notification Detection | ⚠️ Partial | **High** | `DailyNotificationPlugin.swift` - new method |
|
||||
| App Termination Detection | ❌ Missing | **High** | `DailyNotificationPlugin.swift` - recovery logic |
|
||||
| Background Task Registration | ⚠️ Partial | **Medium** | `AppDelegate.swift` - BGTaskScheduler registration |
|
||||
|
||||
### 1.2 Implementation Strategy
|
||||
|
||||
**Phase 1** – Cold start recovery only
|
||||
- Missed notification detection + future notification verification
|
||||
- No termination detection, no boot handling
|
||||
- **See [Phase 1 directive](./ios-implementation-directive-phase1.md) for implementation**
|
||||
|
||||
**Phase 2** – App termination detection & full recovery
|
||||
- Termination detection via UNUserNotificationCenter state comparison
|
||||
- Comprehensive recovery of all schedules (notify + fetch)
|
||||
- Past notifications marked as missed, future notifications rescheduled
|
||||
- **See [Phase 2 directive](./ios-implementation-directive-phase2.md) for implementation**
|
||||
|
||||
**Phase 3** – Background task registration & boot recovery
|
||||
- BGTaskScheduler registration for boot recovery
|
||||
- Next occurrence rescheduled for repeating schedules
|
||||
- **See [Phase 3 directive](./ios-implementation-directive-phase3.md) for implementation**
|
||||
|
||||
---
|
||||
|
||||
## 2. iOS-Specific Considerations
|
||||
|
||||
### 2.1 Key Differences from Android
|
||||
|
||||
**iOS Advantages**:
|
||||
- ✅ Notifications persist across app termination (OS-guaranteed)
|
||||
- ✅ Notifications persist across device reboot (OS-guaranteed)
|
||||
- ✅ No force stop equivalent (iOS doesn't have user-facing force stop)
|
||||
|
||||
**iOS Challenges**:
|
||||
- ❌ App code does NOT run when notification fires (only if user taps)
|
||||
- ❌ Background execution severely limited (BGTaskScheduler only)
|
||||
- ❌ Cannot rely on background execution for recovery
|
||||
- ❌ Must detect missed notifications on app launch
|
||||
|
||||
**Platform Reference**: See [Platform Capability Reference §3](./alarms/01-platform-capability-reference.md#3-ios-notification-capability-matrix) for complete iOS behavior matrix.
|
||||
|
||||
### 2.2 Recovery Scenario Mapping
|
||||
|
||||
**Android → iOS Mapping**:
|
||||
|
||||
| Android Scenario | iOS Equivalent | Detection Method |
|
||||
| ---------------- | -------------- | --------------- |
|
||||
| `COLD_START` | App Launch After Termination | Check if notifications exist vs DB state |
|
||||
| `FORCE_STOP` | App Terminated by System | Check if notifications missing vs DB state |
|
||||
| `BOOT` | Device Reboot | BGTaskScheduler registration (Phase 3) |
|
||||
| `WARM_START` | App Resume (Foreground) | Check app state on resume |
|
||||
|
||||
**Note**: iOS doesn't have a user-facing "force stop" equivalent. System termination is detected by comparing UNUserNotificationCenter state with database state.
|
||||
|
||||
### 2.3 iOS APIs Used
|
||||
|
||||
**Notification Management**:
|
||||
- `UNUserNotificationCenter.current()` - Notification center
|
||||
- `UNUserNotificationCenter.getPendingNotificationRequests()` - Check scheduled notifications
|
||||
- `UNUserNotificationCenter.add()` - Schedule notifications
|
||||
|
||||
**Background Tasks**:
|
||||
- `BGTaskScheduler.shared` - Background task scheduler
|
||||
- `BGTaskScheduler.register()` - Register background task handlers
|
||||
- `BGAppRefreshTaskRequest` - Background fetch requests
|
||||
|
||||
**App Lifecycle**:
|
||||
- `applicationWillTerminate` - App termination notification
|
||||
- `applicationDidBecomeActive` - App foreground notification
|
||||
- `applicationDidEnterBackground` - App background notification
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementation: ReactivationManager (iOS)
|
||||
|
||||
**⚠️ Illustrative only** – See Phase 1 and Phase 2 directives for canonical implementation.
|
||||
|
||||
**ReactivationManager Responsibilities by Phase**:
|
||||
|
||||
| Phase | Responsibilities |
|
||||
| ----- | ---------------- |
|
||||
| 1 | Cold start only (missed detection + verify/reschedule future) |
|
||||
| 2 | Adds termination detection & recovery |
|
||||
| 3 | Background task registration & boot recovery |
|
||||
|
||||
**For implementation details, see**:
|
||||
- [Phase 1: ReactivationManager creation](./ios-implementation-directive-phase1.md#2-implementation-reactivationmanager)
|
||||
- [Phase 2: Termination detection](./ios-implementation-directive-phase2.md#2-implementation-termination-detection)
|
||||
|
||||
### 3.1 Create New File
|
||||
|
||||
**File**: `ios/Plugin/DailyNotificationReactivationManager.swift`
|
||||
|
||||
**Purpose**: Centralized recovery logic for app launch scenarios
|
||||
|
||||
### 3.2 Class Structure
|
||||
|
||||
**⚠️ Illustrative only** – See Phase 1 for canonical implementation.
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
/**
|
||||
* Manages recovery of notifications on app launch
|
||||
* Handles cold start, warm start, and termination recovery scenarios
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class DailyNotificationReactivationManager {
|
||||
|
||||
private static let TAG = "DNP-REACTIVATION"
|
||||
private let notificationCenter = UNUserNotificationCenter.current()
|
||||
private let database: DailyNotificationDatabase
|
||||
private let storage: DailyNotificationStorage
|
||||
|
||||
init(database: DailyNotificationDatabase, storage: DailyNotificationStorage) {
|
||||
self.database = database
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform recovery on app launch
|
||||
* Detects scenario (cold/warm/termination) and handles accordingly
|
||||
*/
|
||||
func performRecovery() async {
|
||||
do {
|
||||
NSLog("\(Self.TAG): Starting app launch recovery")
|
||||
|
||||
// Step 1: Detect scenario
|
||||
let scenario = try await detectScenario()
|
||||
NSLog("\(Self.TAG): Detected scenario: \(scenario)")
|
||||
|
||||
// Step 2: Handle based on scenario
|
||||
switch scenario {
|
||||
case .termination:
|
||||
try await handleTerminationRecovery()
|
||||
case .coldStart:
|
||||
try await handleColdStartRecovery()
|
||||
case .warmStart:
|
||||
try await handleWarmStartRecovery()
|
||||
case .none:
|
||||
NSLog("\(Self.TAG): No recovery needed")
|
||||
}
|
||||
|
||||
NSLog("\(Self.TAG): App launch recovery completed")
|
||||
} catch {
|
||||
NSLog("\(Self.TAG): Error during app launch recovery: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// ... implementation methods below ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Recovery Scenario Detection
|
||||
|
||||
### 4.1 Scenario Detection Algorithm
|
||||
|
||||
**Platform Reference**: [iOS §3.1.1](./alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination) - Notifications survive app termination
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
```swift
|
||||
enum RecoveryScenario {
|
||||
case none // No recovery needed (first launch or warm resume)
|
||||
case coldStart // App launched after termination, notifications may exist
|
||||
case termination // App terminated, notifications missing vs DB
|
||||
case warmStart // App resumed from background (optimization only)
|
||||
}
|
||||
|
||||
func detectScenario() async throws -> RecoveryScenario {
|
||||
// Step 1: Check if database has schedules
|
||||
let schedules = try database.getEnabledSchedules()
|
||||
if schedules.isEmpty {
|
||||
return .none // First launch
|
||||
}
|
||||
|
||||
// Step 2: Get pending notifications from UNUserNotificationCenter
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
|
||||
// Step 3: Compare DB state with notification center state
|
||||
let dbNotificationIds = Set(schedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
// Step 4: Determine scenario
|
||||
if pendingIds.isEmpty && !dbNotificationIds.isEmpty {
|
||||
// DB has schedules but no notifications scheduled
|
||||
return .termination
|
||||
} else if !pendingIds.isEmpty && !dbNotificationIds.isEmpty {
|
||||
// Both have data - check if they match
|
||||
if dbNotificationIds != pendingIds {
|
||||
return .coldStart // Mismatch indicates recovery needed
|
||||
} else {
|
||||
return .warmStart // Match indicates warm resume
|
||||
}
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
```
|
||||
|
||||
**For complete implementation, see**: [Phase 1 directive](./ios-implementation-directive-phase1.md#3-scenario-detection)
|
||||
|
||||
---
|
||||
|
||||
## 5. Missed Notification Detection
|
||||
|
||||
### 5.1 Detection Logic
|
||||
|
||||
**Platform Reference**: [iOS §3.2.1](./alarms/01-platform-capability-reference.md#321-app-code-does-not-run-when-notification-fires) - App code does not run when notification fires
|
||||
|
||||
**iOS Behavior**: When a notification fires, the app code does NOT execute. The notification is displayed, but the app must detect missed notifications on the next app launch.
|
||||
|
||||
**Detection Steps**:
|
||||
|
||||
1. Query database for notifications with `scheduled_time < currentTime`
|
||||
2. Filter for notifications with `delivery_status != 'delivered'`
|
||||
3. Mark as `'missed'` in database
|
||||
4. Record in history table
|
||||
|
||||
**For complete implementation, see**: [Phase 1 directive](./ios-implementation-directive-phase1.md#4-missed-notification-detection)
|
||||
|
||||
---
|
||||
|
||||
## 6. Background Task Registration
|
||||
|
||||
### 6.1 BGTaskScheduler Registration
|
||||
|
||||
**Platform Reference**: [iOS §3.1.3](./alarms/01-platform-capability-reference.md#313-background-tasks-for-prefetching) - Background tasks for prefetching
|
||||
|
||||
**iOS Limitation**: BGTaskScheduler cannot be used for critical scheduling. It's system-controlled and not guaranteed.
|
||||
|
||||
**Use Case**: BGTaskScheduler is used for:
|
||||
- Prefetching content (not critical timing)
|
||||
- Boot recovery (system may defer)
|
||||
- Background maintenance (best effort)
|
||||
|
||||
**Registration Location**: `AppDelegate.swift` or `SceneDelegate.swift`
|
||||
|
||||
**For complete implementation, see**: [Phase 3 directive](./ios-implementation-directive-phase3.md#2-background-task-registration)
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 iOS Testing Tools
|
||||
|
||||
**Simulator Testing**:
|
||||
- `xcrun simctl` - Simulator control
|
||||
- Xcode Instruments - Performance profiling
|
||||
- Console.app - System log viewing
|
||||
|
||||
**Device Testing**:
|
||||
- Xcode Device Console - Real device logs
|
||||
- Settings → Developer → Background App Refresh - Control background execution
|
||||
|
||||
### 7.2 Test Scenarios
|
||||
|
||||
**Phase 1 Tests**:
|
||||
- Cold start recovery
|
||||
- Missed notification detection
|
||||
- Future notification verification
|
||||
|
||||
**Phase 2 Tests**:
|
||||
- App termination detection
|
||||
- Comprehensive recovery
|
||||
- Multiple schedules recovery
|
||||
|
||||
**Phase 3 Tests**:
|
||||
- Background task registration
|
||||
- Boot recovery (simulated)
|
||||
- Background task execution
|
||||
|
||||
**For complete test procedures, see**: [iOS Test Scripts](../test-apps/ios-test-app/test-phase1.sh)
|
||||
|
||||
---
|
||||
|
||||
## 8. Platform-Specific Notes
|
||||
|
||||
### 8.1 Notification Persistence
|
||||
|
||||
**iOS Advantage**: Notifications persist automatically across:
|
||||
- App termination
|
||||
- Device reboot (for calendar/time triggers)
|
||||
|
||||
**App Responsibility**: Must still:
|
||||
- Detect missed notifications on app launch
|
||||
- Reschedule future notifications if needed
|
||||
- Track delivery status in database
|
||||
|
||||
### 8.2 Background Execution Limits
|
||||
|
||||
**iOS Limitation**: Background execution is severely limited:
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Cannot rely on background execution for recovery
|
||||
- Must handle recovery on app launch
|
||||
|
||||
**Workaround**: Use BGTaskScheduler for prefetching only, not for critical scheduling.
|
||||
|
||||
### 8.3 Timing Tolerance
|
||||
|
||||
**iOS Limitation**: Calendar-based notifications have ±180 second tolerance.
|
||||
|
||||
**Impact**: Notifications may fire up to 3 minutes early or late.
|
||||
|
||||
**Mitigation**: Account for tolerance in missed notification detection logic.
|
||||
|
||||
---
|
||||
|
||||
## 9. Next Steps
|
||||
|
||||
1. **Start with Phase 1**: Implement cold start recovery
|
||||
- See [Phase 1 directive](./ios-implementation-directive-phase1.md)
|
||||
- Focus on missed notification detection
|
||||
- Verify future notifications are scheduled
|
||||
|
||||
2. **Proceed to Phase 2**: Add termination detection
|
||||
- See [Phase 2 directive](./ios-implementation-directive-phase2.md)
|
||||
- Implement comprehensive recovery
|
||||
- Handle multiple schedules
|
||||
|
||||
3. **Complete Phase 3**: Background task registration
|
||||
- See [Phase 3 directive](./ios-implementation-directive-phase3.md)
|
||||
- Register BGTaskScheduler handlers
|
||||
- Implement boot recovery
|
||||
|
||||
---
|
||||
|
||||
## 10. References
|
||||
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - iOS OS-level facts
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements this directive implements
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android equivalent for comparison
|
||||
- [iOS Recovery Scenario Mapping](./ios-recovery-scenario-mapping.md) - Detailed scenario mapping
|
||||
- [iOS Core Data Migration Guide](./ios-core-data-migration.md) - Database migration guide
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
423
docs/ios-recovery-scenario-mapping.md
Normal file
423
docs/ios-recovery-scenario-mapping.md
Normal file
@@ -0,0 +1,423 @@
|
||||
# iOS Recovery Scenario Mapping: Android → iOS Equivalents
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - Recovery Scenario Mapping Reference
|
||||
**Version**: 1.0.0
|
||||
**Last Synced With Plugin Version**: v1.1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This document maps Android recovery scenarios to their iOS equivalents, providing a clear translation guide for implementing iOS recovery logic based on Android patterns.
|
||||
|
||||
**Reference**:
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android scenarios
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - iOS scenarios
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
|
||||
---
|
||||
|
||||
## 1. Scenario Mapping Overview
|
||||
|
||||
### 1.1 Direct Mappings
|
||||
|
||||
| Android Scenario | iOS Equivalent | Detection Method | Recovery Action |
|
||||
| ---------------- | -------------- | ---------------- | --------------- |
|
||||
| `COLD_START` | App Launch After Termination | Compare UNUserNotificationCenter vs DB | Detect missed, verify future |
|
||||
| `FORCE_STOP` | App Terminated by System | DB has schedules, no notifications | Full recovery of all schedules |
|
||||
| `BOOT` | Device Reboot | BGTaskScheduler registration | Reschedule all notifications |
|
||||
| `WARM_START` | App Resume (Foreground) | Notifications match DB state | No recovery needed (optimization) |
|
||||
| `NONE` | First Launch / No Recovery | Empty database | No action needed |
|
||||
|
||||
### 1.2 Key Differences
|
||||
|
||||
**iOS Advantages**:
|
||||
- ✅ Notifications persist across termination (OS-guaranteed)
|
||||
- ✅ Notifications persist across reboot (OS-guaranteed)
|
||||
- ❌ No user-facing "force stop" equivalent
|
||||
|
||||
**iOS Challenges**:
|
||||
- ❌ App code does NOT run when notification fires
|
||||
- ❌ Must detect missed notifications on app launch
|
||||
- ❌ Background execution severely limited
|
||||
|
||||
---
|
||||
|
||||
## 2. Detailed Scenario Mappings
|
||||
|
||||
### 2.1 COLD_START → App Launch After Termination
|
||||
|
||||
**Android Definition**:
|
||||
- Process killed, alarms may or may not exist
|
||||
- Database still populated
|
||||
- Alarms may have been cleared by OS
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App terminated by system or user
|
||||
- Notifications may still exist (OS-guaranteed persistence)
|
||||
- Database still populated
|
||||
- Need to verify notification state matches database
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist in AlarmManager
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (alarmsExist && dbHasSchedules) {
|
||||
return COLD_START
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist in UNUserNotificationCenter
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
if !pendingNotifications.isEmpty && !dbSchedules.isEmpty {
|
||||
// Compare notification IDs with DB state
|
||||
let dbIds = Set(dbSchedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
if dbIds != pendingIds {
|
||||
return .coldStart // Mismatch indicates recovery needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Detect missed notifications (scheduled_time < now, not delivered)
|
||||
2. Mark missed notifications in database
|
||||
3. Verify future notifications are scheduled
|
||||
4. Reschedule missing future notifications
|
||||
|
||||
**Platform Reference**: [iOS §3.1.1](./alarms/01-platform-capability-reference.md#311-notifications-survive-app-termination)
|
||||
|
||||
---
|
||||
|
||||
### 2.2 FORCE_STOP → App Terminated by System
|
||||
|
||||
**Android Definition**:
|
||||
- User force-stopped app via Settings
|
||||
- All alarms cleared
|
||||
- Database still populated
|
||||
- Boot receiver blocked until user launches app
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App terminated by system (low memory, etc.)
|
||||
- Notifications may be missing (system cleared them)
|
||||
- Database still populated
|
||||
- No user-facing force stop equivalent
|
||||
|
||||
**Key Difference**: iOS doesn't have a user-facing "force stop" option. System termination is the closest equivalent.
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (!alarmsExist && dbHasSchedules && !isBootRecent) {
|
||||
return FORCE_STOP
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
if pendingNotifications.isEmpty && !dbSchedules.isEmpty {
|
||||
// DB has schedules but no notifications scheduled
|
||||
return .termination
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Detect all missed notifications
|
||||
2. Mark all missed notifications in database
|
||||
3. Reschedule all future notifications
|
||||
4. Reschedule all fetch schedules (if applicable)
|
||||
|
||||
**Platform Reference**: [iOS §3.2.1](./alarms/01-platform-capability-reference.md#321-app-code-does-not-run-when-notification-fires)
|
||||
|
||||
---
|
||||
|
||||
### 2.3 BOOT → Device Reboot
|
||||
|
||||
**Android Definition**:
|
||||
- Device rebooted
|
||||
- All alarms wiped (OS behavior)
|
||||
- Database still populated
|
||||
- Boot receiver executes after boot completes
|
||||
|
||||
**iOS Equivalent**:
|
||||
- Device rebooted
|
||||
- Notifications persist automatically (OS-guaranteed)
|
||||
- Database still populated
|
||||
- BGTaskScheduler may execute (system-controlled)
|
||||
|
||||
**Key Difference**: iOS automatically persists notifications across reboot. Android requires manual rescheduling.
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check boot flag (set by BootReceiver)
|
||||
val bootFlag = sharedPreferences.getLong("last_boot_time", 0)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (bootFlag > 0 && (currentTime - bootFlag) < 60000) {
|
||||
return BOOT
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// BGTaskScheduler registration handles boot
|
||||
// Check if this is a boot-triggered background task
|
||||
if isBootBackgroundTask {
|
||||
return .boot
|
||||
}
|
||||
|
||||
// Or detect on app launch after reboot
|
||||
let lastLaunchTime = UserDefaults.standard.double(forKey: "last_launch_time")
|
||||
let bootTime = ProcessInfo.processInfo.systemUptime
|
||||
if lastLaunchTime > 0 && bootTime < 60 {
|
||||
return .boot
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
1. Verify notifications still exist (iOS usually handles this)
|
||||
2. Detect any missed notifications during reboot window
|
||||
3. Reschedule any missing notifications
|
||||
4. Update next run times for repeating schedules
|
||||
|
||||
**Platform Reference**: [iOS §3.1.2](./alarms/01-platform-capability-reference.md#312-notifications-persist-across-device-reboot)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 WARM_START → App Resume (Foreground)
|
||||
|
||||
**Android Definition**:
|
||||
- App resumed from background
|
||||
- Alarms still exist
|
||||
- Database matches alarm state
|
||||
- No recovery needed (optimization)
|
||||
|
||||
**iOS Equivalent**:
|
||||
- App resumed from background
|
||||
- Notifications still exist
|
||||
- Database matches notification state
|
||||
- No recovery needed (optimization)
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if alarms exist and match DB
|
||||
val alarmsExist = alarmManager.hasAlarm(pendingIntent)
|
||||
if (alarmsExist && dbMatchesAlarms) {
|
||||
return WARM_START
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if notifications exist and match DB
|
||||
let pendingNotifications = try await notificationCenter.pendingNotificationRequests()
|
||||
let dbSchedules = try database.getEnabledSchedules()
|
||||
|
||||
let dbIds = Set(dbSchedules.flatMap { $0.getScheduledNotificationIds() })
|
||||
let pendingIds = Set(pendingNotifications.map { $0.identifier })
|
||||
|
||||
if dbIds == pendingIds {
|
||||
return .warmStart // Match indicates warm resume
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
- None (optimization only)
|
||||
- May perform lightweight verification
|
||||
- May update metrics
|
||||
|
||||
---
|
||||
|
||||
### 2.5 NONE → First Launch / No Recovery
|
||||
|
||||
**Android Definition**:
|
||||
- First app launch
|
||||
- Empty database
|
||||
- No schedules configured
|
||||
- No recovery needed
|
||||
|
||||
**iOS Equivalent**:
|
||||
- First app launch
|
||||
- Empty database
|
||||
- No schedules configured
|
||||
- No recovery needed
|
||||
|
||||
**Detection Logic**:
|
||||
|
||||
**Android**:
|
||||
```kotlin
|
||||
// Check if database is empty
|
||||
val schedules = database.scheduleDao().getEnabled()
|
||||
if (schedules.isEmpty()) {
|
||||
return NONE
|
||||
}
|
||||
```
|
||||
|
||||
**iOS**:
|
||||
```swift
|
||||
// Check if database is empty
|
||||
let schedules = try database.getEnabledSchedules()
|
||||
if schedules.isEmpty {
|
||||
return .none
|
||||
}
|
||||
```
|
||||
|
||||
**Recovery Actions**:
|
||||
- None
|
||||
|
||||
---
|
||||
|
||||
## 3. Recovery Action Mapping
|
||||
|
||||
### 3.1 Missed Notification Detection
|
||||
|
||||
**Android**:
|
||||
- Query AlarmManager for past alarms
|
||||
- Check database for undelivered notifications
|
||||
- Mark as missed in database
|
||||
|
||||
**iOS**:
|
||||
- Query database for past scheduled notifications
|
||||
- Check delivery status
|
||||
- Mark as missed in database
|
||||
|
||||
**Key Difference**: iOS cannot query past notifications from UNUserNotificationCenter. Must rely on database state.
|
||||
|
||||
### 3.2 Future Notification Verification
|
||||
|
||||
**Android**:
|
||||
- Query AlarmManager for future alarms
|
||||
- Compare with database schedules
|
||||
- Reschedule missing alarms
|
||||
|
||||
**iOS**:
|
||||
- Query UNUserNotificationCenter for pending notifications
|
||||
- Compare with database schedules
|
||||
- Reschedule missing notifications
|
||||
|
||||
**Key Difference**: iOS uses UNUserNotificationCenter instead of AlarmManager.
|
||||
|
||||
### 3.3 Full Recovery
|
||||
|
||||
**Android**:
|
||||
- Reschedule all notify schedules
|
||||
- Reschedule all fetch schedules (WorkManager)
|
||||
- Mark past notifications as missed
|
||||
|
||||
**iOS**:
|
||||
- Reschedule all notify schedules
|
||||
- Reschedule all fetch schedules (BGTaskScheduler)
|
||||
- Mark past notifications as missed
|
||||
|
||||
**Key Difference**: iOS uses BGTaskScheduler instead of WorkManager.
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation Checklist
|
||||
|
||||
### 4.1 Phase 1: Cold Start Recovery
|
||||
|
||||
- [ ] Implement scenario detection (cold start)
|
||||
- [ ] Implement missed notification detection
|
||||
- [ ] Implement future notification verification
|
||||
- [ ] Test cold start recovery
|
||||
|
||||
### 4.2 Phase 2: Termination Detection
|
||||
|
||||
- [ ] Implement termination detection
|
||||
- [ ] Implement full recovery logic
|
||||
- [ ] Test termination recovery
|
||||
|
||||
### 4.3 Phase 3: Boot Recovery
|
||||
|
||||
- [ ] Implement BGTaskScheduler registration
|
||||
- [ ] Implement boot detection
|
||||
- [ ] Test boot recovery
|
||||
|
||||
---
|
||||
|
||||
## 5. Platform-Specific Notes
|
||||
|
||||
### 5.1 iOS Advantages
|
||||
|
||||
1. **Notification Persistence**: iOS automatically persists notifications across termination and reboot
|
||||
2. **No Force Stop**: iOS doesn't have user-facing force stop, reducing complexity
|
||||
3. **Simplified Recovery**: Less recovery needed due to OS persistence
|
||||
|
||||
### 5.2 iOS Challenges
|
||||
|
||||
1. **No Code Execution on Fire**: App code doesn't run when notification fires
|
||||
2. **Background Limits**: Severely limited background execution
|
||||
3. **Timing Tolerance**: ±180 second tolerance for calendar triggers
|
||||
|
||||
### 5.3 Android Advantages
|
||||
|
||||
1. **Code Execution on Fire**: PendingIntent can execute code when alarm fires
|
||||
2. **WorkManager**: More reliable background execution
|
||||
3. **Exact Timing**: Can achieve exact timing with permission
|
||||
|
||||
### 5.4 Android Challenges
|
||||
|
||||
1. **No Persistence**: Alarms don't persist across reboot
|
||||
2. **Force Stop**: Hard kill that cannot be bypassed
|
||||
3. **Boot Recovery**: Must implement boot receiver
|
||||
|
||||
---
|
||||
|
||||
## 6. Testing Strategy
|
||||
|
||||
### 6.1 Scenario Testing
|
||||
|
||||
**Cold Start**:
|
||||
1. Terminate app (swipe away)
|
||||
2. Wait for notification time to pass
|
||||
3. Launch app
|
||||
4. Verify missed notification detection
|
||||
5. Verify future notifications rescheduled
|
||||
|
||||
**Termination**:
|
||||
1. Schedule notifications
|
||||
2. Terminate app
|
||||
3. Clear notifications (simulate system clearing)
|
||||
4. Launch app
|
||||
5. Verify full recovery
|
||||
|
||||
**Boot**:
|
||||
1. Schedule notifications
|
||||
2. Reboot device (or simulate)
|
||||
3. Launch app
|
||||
4. Verify notifications still exist
|
||||
5. Verify any missed notifications detected
|
||||
|
||||
---
|
||||
|
||||
## 7. References
|
||||
|
||||
- [Android Implementation Directive](./android-implementation-directive.md) - Android scenarios
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - iOS scenarios
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - OS-level facts
|
||||
- [Plugin Requirements](./alarms/03-plugin-requirements.md) - Requirements
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
574
docs/ios-troubleshooting-guide.md
Normal file
574
docs/ios-troubleshooting-guide.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# iOS Troubleshooting Guide
|
||||
|
||||
**Author**: Matthew Raymer
|
||||
**Date**: 2025-12-08
|
||||
**Status**: 🎯 **ACTIVE** - iOS Troubleshooting Reference
|
||||
**Version**: 1.0.0
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide provides solutions to common iOS-specific issues when using the Daily Notification Plugin. It covers debugging techniques, common problems, and their solutions.
|
||||
|
||||
**Reference**:
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md) - Implementation details
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md) - iOS OS behaviors
|
||||
- [iOS Logging Guide](../doc/test-app-ios/IOS_LOGGING_GUIDE.md) - How to view logs
|
||||
|
||||
---
|
||||
|
||||
## 1. Common Issues
|
||||
|
||||
### 1.1 Notifications Not Firing
|
||||
|
||||
**Symptoms:**
|
||||
- Notifications scheduled but don't appear
|
||||
- No notification at scheduled time
|
||||
- Notifications work in simulator but not on device
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check Notification Permission:**
|
||||
```swift
|
||||
// In app code
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
print("Authorization status: \(settings.authorizationStatus)")
|
||||
}
|
||||
```
|
||||
|
||||
Or check in Xcode Console:
|
||||
```
|
||||
DNP-PLUGIN: Notification permission status: authorized
|
||||
```
|
||||
|
||||
2. **Check Pending Notifications:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
print("Pending notifications: \(requests.count)")
|
||||
}
|
||||
```
|
||||
|
||||
3. **Check Background App Refresh:**
|
||||
- Settings → [Your App] → Background App Refresh
|
||||
- Must be enabled for background tasks
|
||||
|
||||
4. **Check Notification Limit:**
|
||||
- iOS limits to 64 pending notifications
|
||||
- Check if limit is exceeded
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Permission Denied:**
|
||||
- Request permission: `DailyNotification.requestNotificationPermission()`
|
||||
- Guide user to Settings → [Your App] → Notifications
|
||||
|
||||
- **Background App Refresh Disabled:**
|
||||
- Guide user to enable: Settings → [Your App] → Background App Refresh
|
||||
- Or use: `DailyNotification.openBackgroundAppRefreshSettings()`
|
||||
|
||||
- **Notification Limit Exceeded:**
|
||||
- Reduce number of scheduled notifications
|
||||
- Implement notification cleanup logic
|
||||
- Check rolling window implementation
|
||||
|
||||
- **Simulator vs Device:**
|
||||
- Simulator may not fire notifications reliably
|
||||
- Test on physical device for accurate behavior
|
||||
|
||||
---
|
||||
|
||||
### 1.2 Background Tasks Not Executing
|
||||
|
||||
**Symptoms:**
|
||||
- Prefetch tasks don't run
|
||||
- Background fetch never executes
|
||||
- BGTaskScheduler tasks not firing
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check BGTaskScheduler Registration:**
|
||||
```swift
|
||||
// Verify registration in AppDelegate
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.timesafari.dailynotification.fetch", using: nil) { task in
|
||||
// Handler should be registered
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check Info.plist:**
|
||||
```xml
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>com.timesafari.dailynotification.fetch</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
3. **Check Background App Refresh:**
|
||||
- Must be enabled in Settings
|
||||
- System-controlled timing (not guaranteed)
|
||||
|
||||
4. **Check Logs:**
|
||||
```
|
||||
DNP-FETCH-START: Background fetch task started
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Not Registered:**
|
||||
- Verify registration in `AppDelegate.application(_:didFinishLaunchingWithOptions:)`
|
||||
- Check Info.plist has correct identifiers
|
||||
|
||||
- **Background App Refresh Disabled:**
|
||||
- User must enable in Settings
|
||||
- Cannot be programmatically enabled
|
||||
|
||||
- **System Not Executing:**
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Execution timing is not guaranteed
|
||||
- System may defer or skip tasks
|
||||
- Use for prefetching only, not critical scheduling
|
||||
|
||||
- **Simulator Limitations:**
|
||||
- Background tasks may not execute in simulator
|
||||
- Test on physical device
|
||||
|
||||
---
|
||||
|
||||
### 1.3 Notifications Disappear After App Termination
|
||||
|
||||
**Symptoms:**
|
||||
- Notifications scheduled but disappear when app is terminated
|
||||
- Notifications don't persist across app restarts
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
**This should NOT happen on iOS** - notifications persist automatically (OS-guaranteed).
|
||||
|
||||
**If it happens, check:**
|
||||
|
||||
1. **Notification Trigger Type:**
|
||||
- Calendar/time triggers persist ✅
|
||||
- Location triggers do NOT persist ❌
|
||||
|
||||
2. **Notification Content:**
|
||||
- Ensure notification has valid content
|
||||
- Check for invalid trigger dates
|
||||
|
||||
3. **System Storage:**
|
||||
- iOS may clear notifications if device storage is full
|
||||
- Check available storage
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Use Calendar Triggers:**
|
||||
```swift
|
||||
// ✅ Persists across reboot
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||||
|
||||
// ❌ Does NOT persist
|
||||
let trigger = UNLocationNotificationTrigger(region: region, repeats: true)
|
||||
```
|
||||
|
||||
- **Check Device Storage:**
|
||||
- Free up storage if device is full
|
||||
- iOS may clear notifications when storage is critical
|
||||
|
||||
---
|
||||
|
||||
### 1.4 Recovery Not Working
|
||||
|
||||
**Symptoms:**
|
||||
- Missed notifications not detected on app launch
|
||||
- Future notifications not verified/rescheduled
|
||||
- No recovery activity in logs
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
**Recovery features are NOT yet implemented** (as of 2025-12-08).
|
||||
|
||||
**Expected Behavior (Once Implemented):**
|
||||
|
||||
1. **Check Logs for Recovery:**
|
||||
```
|
||||
DNP-REACTIVATION: Starting app launch recovery
|
||||
DNP-REACTIVATION: Detected scenario: coldStart
|
||||
DNP-REACTIVATION: Missed notifications detected: 2
|
||||
DNP-REACTIVATION: Future notifications verified: 1
|
||||
```
|
||||
|
||||
2. **Verify Recovery Logic:**
|
||||
- Should run in `DailyNotificationPlugin.load()`
|
||||
- Should detect missed notifications
|
||||
- Should verify future notifications
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Implementation Pending:**
|
||||
- See [iOS Implementation Directive Phase 1](./ios-implementation-directive-phase1.md)
|
||||
- Recovery features need to be implemented
|
||||
|
||||
- **Manual Workaround:**
|
||||
- Check pending notifications manually
|
||||
- Reschedule if needed
|
||||
|
||||
---
|
||||
|
||||
### 1.5 Database/Storage Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Database errors in logs
|
||||
- Data not persisting
|
||||
- Core Data errors
|
||||
|
||||
**Diagnosis Steps:**
|
||||
|
||||
1. **Check Database Path:**
|
||||
```swift
|
||||
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
let dbPath = documentsPath.appendingPathComponent("daily_notifications.db")
|
||||
print("Database path: \(dbPath.path)")
|
||||
```
|
||||
|
||||
2. **Check Core Data Stack:**
|
||||
- Verify `NSPersistentContainer` initialization
|
||||
- Check for migration errors
|
||||
|
||||
3. **Check Logs:**
|
||||
```
|
||||
DNP-STORAGE: Database opened successfully
|
||||
DNP-STORAGE: Error opening database: [error]
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Database Path Issues:**
|
||||
- Verify app has write permissions
|
||||
- Check Documents directory is accessible
|
||||
- Ensure path is correct
|
||||
|
||||
- **Core Data Errors:**
|
||||
- Check Core Data model version
|
||||
- Verify migration policies
|
||||
- Check for schema mismatches
|
||||
|
||||
- **Storage Full:**
|
||||
- Free up device storage
|
||||
- iOS may clear app data if storage is critical
|
||||
|
||||
---
|
||||
|
||||
### 1.6 Permission Issues
|
||||
|
||||
**Symptoms:**
|
||||
- Permission requests fail
|
||||
- Permission status incorrect
|
||||
- Cannot schedule notifications
|
||||
|
||||
**Diagnosis:**
|
||||
|
||||
1. **Check Current Status:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
switch settings.authorizationStatus {
|
||||
case .authorized: // ✅ Can schedule
|
||||
case .denied: // ❌ User denied
|
||||
case .notDetermined: // ⚠️ Not requested yet
|
||||
case .provisional: // ⚠️ Provisional (iOS 12+)
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check Logs:**
|
||||
```
|
||||
DNP-PLUGIN: Notification permission status: denied
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
- **Permission Denied:**
|
||||
- Cannot request again programmatically
|
||||
- Guide user to Settings → [Your App] → Notifications
|
||||
- Use: `DailyNotification.openNotificationSettings()`
|
||||
|
||||
- **Not Determined:**
|
||||
- Request permission: `DailyNotification.requestNotificationPermission()`
|
||||
- Show explanation before requesting
|
||||
|
||||
- **Provisional:**
|
||||
- iOS 12+ feature
|
||||
- Notifications delivered quietly
|
||||
- User can upgrade to full permission
|
||||
|
||||
---
|
||||
|
||||
## 2. Debugging Techniques
|
||||
|
||||
### 2.1 Viewing Logs
|
||||
|
||||
**Xcode Console (Recommended):**
|
||||
1. Run app in Xcode (Cmd+R)
|
||||
2. Open Debug Area (Cmd+Shift+Y)
|
||||
3. Filter by: `DNP-` or `DailyNotification`
|
||||
|
||||
**Console.app:**
|
||||
1. Open Console.app
|
||||
2. Select device/simulator
|
||||
3. Filter by process: `ios-test-app`
|
||||
|
||||
**Command Line:**
|
||||
```bash
|
||||
# Simulator logs
|
||||
xcrun simctl spawn <device-id> log stream --level=debug --predicate 'processImagePath contains "ios-test-app"'
|
||||
|
||||
# Device logs (requires device connected)
|
||||
xcrun devicectl device process monitor --device <device-id> --filter "ios-test-app"
|
||||
```
|
||||
|
||||
### 2.2 Checking Pending Notifications
|
||||
|
||||
**Via Plugin Method:**
|
||||
```typescript
|
||||
const result = await DailyNotification.getPendingNotifications();
|
||||
console.log(`Pending: ${result.count}`);
|
||||
```
|
||||
|
||||
**Via Swift Code:**
|
||||
```swift
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests { requests in
|
||||
print("Pending notifications: \(requests.count)")
|
||||
for request in requests {
|
||||
print(" - \(request.identifier): \(request.content.title)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Checking Background Task Status
|
||||
|
||||
**Via Plugin Method:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getBackgroundTaskStatus();
|
||||
console.log(`Fetch task registered: ${status.fetchTaskRegistered}`);
|
||||
console.log(`Background refresh enabled: ${status.backgroundRefreshEnabled}`);
|
||||
```
|
||||
|
||||
**Via Swift Code:**
|
||||
```swift
|
||||
// Check registration
|
||||
let registered = BGTaskScheduler.shared.registeredTaskIdentifiers
|
||||
print("Registered tasks: \(registered)")
|
||||
|
||||
// Check Background App Refresh (requires entitlement)
|
||||
// Cannot check programmatically - must guide user to Settings
|
||||
```
|
||||
|
||||
### 2.4 Simulating Background Tasks (Simulator Only)
|
||||
|
||||
**LLDB Command in Xcode:**
|
||||
```lldb
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]
|
||||
```
|
||||
|
||||
**Note:** This only works in simulator, not on physical devices.
|
||||
|
||||
---
|
||||
|
||||
## 3. Platform-Specific Considerations
|
||||
|
||||
### 3.1 Simulator vs Device
|
||||
|
||||
**Simulator Limitations:**
|
||||
- Background tasks may not execute reliably
|
||||
- Notifications may not fire at exact time
|
||||
- Some features require physical device
|
||||
|
||||
**Device Testing:**
|
||||
- More accurate behavior
|
||||
- Background tasks execute (system-controlled)
|
||||
- Notifications fire reliably
|
||||
|
||||
**Recommendation:** Test critical features on physical device.
|
||||
|
||||
### 3.2 iOS Version Differences
|
||||
|
||||
**iOS 12+:**
|
||||
- Provisional notification authorization
|
||||
- Background task improvements
|
||||
|
||||
**iOS 13+:**
|
||||
- State actor support (concurrency)
|
||||
- Improved background execution
|
||||
|
||||
**iOS 14+:**
|
||||
- Notification interruption levels
|
||||
- Focus modes (may affect notifications)
|
||||
|
||||
**iOS 15+:**
|
||||
- Notification summary
|
||||
- Focus mode integration
|
||||
|
||||
### 3.3 Background Execution Limits
|
||||
|
||||
**iOS Constraints:**
|
||||
- BGTaskScheduler is system-controlled
|
||||
- Execution timing not guaranteed
|
||||
- Minimum intervals between tasks (hours)
|
||||
- Tasks may be deferred or skipped
|
||||
|
||||
**Workaround:**
|
||||
- Use BGTaskScheduler for prefetching only
|
||||
- Don't rely on it for critical scheduling
|
||||
- Use UNUserNotificationCenter for notifications (more reliable)
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Codes
|
||||
|
||||
### 4.1 Common Error Codes
|
||||
|
||||
| Error Code | Description | Solution |
|
||||
| ---------- | ----------- | -------- |
|
||||
| `NOTIFICATION_PERMISSION_DENIED` | User denied notification permission | Guide user to Settings |
|
||||
| `BACKGROUND_REFRESH_DISABLED` | Background App Refresh disabled | Guide user to enable in Settings |
|
||||
| `PENDING_NOTIFICATION_LIMIT_EXCEEDED` | Exceeded 64 notification limit | Reduce scheduled notifications |
|
||||
| `BG_TASK_NOT_REGISTERED` | Background task not registered | Check Info.plist and AppDelegate |
|
||||
| `BG_TASK_EXECUTION_FAILED` | Background task execution failed | Check logs for specific error |
|
||||
|
||||
### 4.2 Checking Error Details
|
||||
|
||||
**Via Logs:**
|
||||
```
|
||||
DNP-ERROR: [Error Code] [Error Message]
|
||||
DNP-ERROR: NOTIFICATION_PERMISSION_DENIED: User denied notification permission
|
||||
```
|
||||
|
||||
**Via Plugin:**
|
||||
```typescript
|
||||
try {
|
||||
await DailyNotification.scheduleDailyNotification({...});
|
||||
} catch (error) {
|
||||
console.error('Error code:', error.code);
|
||||
console.error('Error message:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Performance Issues
|
||||
|
||||
### 5.1 Slow Notification Scheduling
|
||||
|
||||
**Symptoms:**
|
||||
- Scheduling takes too long
|
||||
- App freezes during scheduling
|
||||
|
||||
**Solutions:**
|
||||
- Schedule notifications asynchronously
|
||||
- Batch operations when possible
|
||||
- Use background queue for heavy operations
|
||||
|
||||
### 5.2 High Memory Usage
|
||||
|
||||
**Symptoms:**
|
||||
- App memory usage high
|
||||
- Memory warnings in logs
|
||||
|
||||
**Solutions:**
|
||||
- Implement notification cleanup
|
||||
- Limit cached notifications
|
||||
- Use efficient data structures
|
||||
|
||||
### 5.3 Battery Drain
|
||||
|
||||
**Symptoms:**
|
||||
- Battery drains quickly
|
||||
- Background activity high
|
||||
|
||||
**Solutions:**
|
||||
- Limit background task frequency
|
||||
- Optimize prefetch operations
|
||||
- Use efficient scheduling algorithms
|
||||
|
||||
---
|
||||
|
||||
## 6. Getting Help
|
||||
|
||||
### 6.1 Log Collection
|
||||
|
||||
**Collect Logs:**
|
||||
1. Reproduce the issue
|
||||
2. Collect logs from Xcode Console or Console.app
|
||||
3. Filter by `DNP-` prefix
|
||||
4. Include relevant error messages
|
||||
|
||||
**Log Format:**
|
||||
```
|
||||
DNP-PLUGIN: [Message]
|
||||
DNP-ERROR: [Error Code] [Error Message]
|
||||
DNP-REACTIVATION: [Recovery Activity]
|
||||
```
|
||||
|
||||
### 6.2 Issue Reporting
|
||||
|
||||
**Include:**
|
||||
- iOS version
|
||||
- Device model (or simulator)
|
||||
- Plugin version
|
||||
- Steps to reproduce
|
||||
- Relevant logs
|
||||
- Expected vs actual behavior
|
||||
|
||||
### 6.3 Documentation References
|
||||
|
||||
- [iOS Implementation Directive](./ios-implementation-directive.md)
|
||||
- [Platform Capability Reference](./alarms/01-platform-capability-reference.md)
|
||||
- [API Reference](../API.md)
|
||||
- [iOS Logging Guide](../doc/test-app-ios/IOS_LOGGING_GUIDE.md)
|
||||
|
||||
---
|
||||
|
||||
## 7. Quick Reference
|
||||
|
||||
### 7.1 Common Commands
|
||||
|
||||
**Check Pending Notifications:**
|
||||
```bash
|
||||
# Via plugin method (recommended)
|
||||
# Or check logs for scheduling activity
|
||||
```
|
||||
|
||||
**View Logs:**
|
||||
```bash
|
||||
# Xcode Console (Cmd+Shift+Y)
|
||||
# Filter: DNP-
|
||||
|
||||
# Console.app
|
||||
# Filter: ios-test-app
|
||||
```
|
||||
|
||||
**Check Permissions:**
|
||||
```typescript
|
||||
const status = await DailyNotification.getNotificationPermissionStatus();
|
||||
```
|
||||
|
||||
**Open Settings:**
|
||||
```typescript
|
||||
await DailyNotification.openNotificationSettings();
|
||||
await DailyNotification.openBackgroundAppRefreshSettings();
|
||||
```
|
||||
|
||||
### 7.2 Checklist
|
||||
|
||||
**Before Reporting Issue:**
|
||||
- [ ] Checked notification permissions
|
||||
- [ ] Verified Background App Refresh is enabled
|
||||
- [ ] Checked pending notification count (< 64)
|
||||
- [ ] Reviewed logs for errors
|
||||
- [ ] Tested on physical device (not just simulator)
|
||||
- [ ] Verified iOS version compatibility
|
||||
- [ ] Checked device storage availability
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0.0
|
||||
**Last Updated**: 2025-12-08
|
||||
**Next Review**: After Phase 1 implementation
|
||||
|
||||
Reference in New Issue
Block a user