feat(ios): show notifications in foreground and add visual feedback
Implement UNUserNotificationCenterDelegate in AppDelegate to display notifications when app is in foreground. Add visual feedback indicator in test app UI to confirm notification delivery. Changes: - AppDelegate: Conform to UNUserNotificationCenterDelegate protocol - AppDelegate: Implement willPresent and didReceive delegate methods - AppDelegate: Set delegate at multiple lifecycle points to ensure it's always active (immediate, after Capacitor init, on app active) - UI: Add notification received indicator in status card - UI: Add periodic check for notification delivery (every 5 seconds) - UI: Add instructions on where to look for notification banner - Docs: Add IOS_LOGGING_GUIDE.md for debugging iOS logs This fixes the issue where scheduled notifications were not visible when the app was in the foreground. The delegate method now properly presents notifications with banner, sound, and badge options. Verified working: Logs show delegate method called successfully when notification fires, with proper presentation options set.
This commit is contained in:
283
doc/test-app-ios/IOS_LOGGING_GUIDE.md
Normal file
283
doc/test-app-ios/IOS_LOGGING_GUIDE.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# iOS Logging Guide - How to Check Logs for Errors
|
||||
|
||||
**Purpose:** Quick reference for viewing iOS app logs during development and debugging
|
||||
|
||||
**Last Updated:** 2025-11-15
|
||||
**Status:** 🎯 **ACTIVE** - Reference guide for iOS debugging
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Most Common Methods (in order of ease):**
|
||||
|
||||
1. **Xcode Console** (when app is running in Xcode) - Easiest
|
||||
2. **Console.app** (macOS system console) - Good for background logs
|
||||
3. **Command-line** (`xcrun simctl`) - Best for automation/scripts
|
||||
|
||||
---
|
||||
|
||||
## Method 1: Xcode Console (Recommended for Development)
|
||||
|
||||
**When to use:** App is running in Xcode (simulator or device)
|
||||
|
||||
### Steps:
|
||||
|
||||
1. **Open Xcode** and run your app (Cmd+R)
|
||||
2. **Open Debug Area:**
|
||||
- Press **Cmd+Shift+Y** (or View → Debug Area → Activate Console)
|
||||
- Or click the bottom panel icon in Xcode
|
||||
3. **Filter logs:**
|
||||
- Click the search box at bottom of console
|
||||
- Type: `DNP-` or `DailyNotification` or `Error`
|
||||
- Press Enter
|
||||
|
||||
### Filter Examples:
|
||||
|
||||
```
|
||||
DNP-PLUGIN # Plugin operations
|
||||
DNP-FETCH # Background fetch operations
|
||||
DNP-SCHEDULER # Scheduling operations
|
||||
DNP-STORAGE # Storage operations
|
||||
Error # All errors
|
||||
```
|
||||
|
||||
### Copy-Paste Commands (LLDB Console):
|
||||
|
||||
When app is running, you can also use LLDB commands in Xcode console:
|
||||
|
||||
```swift
|
||||
// Check pending notifications
|
||||
po UNUserNotificationCenter.current().pendingNotificationRequests()
|
||||
|
||||
// Check permission status
|
||||
po await UNUserNotificationCenter.current().notificationSettings()
|
||||
|
||||
// Manually trigger BGTask (simulator only)
|
||||
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method 2: Console.app (macOS System Console)
|
||||
|
||||
**When to use:** App is running in background, or you want to see system-level logs
|
||||
|
||||
### Steps:
|
||||
|
||||
1. **Open Console.app:**
|
||||
- Press **Cmd+Space** (Spotlight)
|
||||
- Type: `Console`
|
||||
- Press Enter
|
||||
- Or: Applications → Utilities → Console
|
||||
|
||||
2. **Select Device/Simulator:**
|
||||
- In left sidebar, expand "Devices"
|
||||
- Select your simulator or connected device
|
||||
- Or select "All Logs" for system-wide logs
|
||||
|
||||
3. **Filter logs:**
|
||||
- Click search box (top right)
|
||||
- Type: `DNP-` or `com.timesafari.dailynotification`
|
||||
- Press Enter
|
||||
|
||||
### Filter by Subsystem (Structured Logging):
|
||||
|
||||
The plugin uses structured logging with subsystems:
|
||||
|
||||
```
|
||||
com.timesafari.dailynotification.plugin # Plugin operations
|
||||
com.timesafari.dailynotification.fetch # Fetch operations
|
||||
com.timesafari.dailynotification.scheduler # Scheduling operations
|
||||
com.timesafari.dailynotification.storage # Storage operations
|
||||
```
|
||||
|
||||
**To filter by subsystem:**
|
||||
- In Console.app search: `subsystem:com.timesafari.dailynotification`
|
||||
|
||||
---
|
||||
|
||||
## Method 3: Command-Line (xcrun simctl)
|
||||
|
||||
**When to use:** Automation, scripts, or when Xcode/Console.app aren't available
|
||||
|
||||
### Stream Live Logs (Simulator):
|
||||
|
||||
```bash
|
||||
# Stream all logs from booted simulator
|
||||
xcrun simctl spawn booted log stream
|
||||
|
||||
# Stream only plugin logs (filtered)
|
||||
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"'
|
||||
|
||||
# Stream with DNP- prefix filter
|
||||
xcrun simctl spawn booted log stream | grep "DNP-"
|
||||
```
|
||||
|
||||
### Save Logs to File:
|
||||
|
||||
```bash
|
||||
# Save all logs to file
|
||||
xcrun simctl spawn booted log stream > device.log 2>&1
|
||||
|
||||
# Save filtered logs
|
||||
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"' > plugin.log 2>&1
|
||||
|
||||
# Then analyze with grep
|
||||
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log
|
||||
```
|
||||
|
||||
### View Recent Logs (Not Streaming):
|
||||
|
||||
```bash
|
||||
# Show recent logs (last 100 lines)
|
||||
xcrun simctl spawn booted log show --last 1m | grep "DNP-"
|
||||
|
||||
# Show logs for specific time range
|
||||
xcrun simctl spawn booted log show --start "2025-11-15 10:00:00" --end "2025-11-15 11:00:00" | grep "DNP-"
|
||||
```
|
||||
|
||||
### Physical Device Logs:
|
||||
|
||||
```bash
|
||||
# List connected devices
|
||||
xcrun devicectl list devices
|
||||
|
||||
# Stream logs from physical device (requires device UDID)
|
||||
xcrun devicectl device process launch --device <UDID> com.timesafari.dailynotification.test
|
||||
|
||||
# Or use Console.app for physical devices (easier)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Method 4: Validate Log Sequence (Automated)
|
||||
|
||||
**When to use:** Testing prefetch cycles, verifying complete execution
|
||||
|
||||
### Using Validation Script:
|
||||
|
||||
```bash
|
||||
# From log file
|
||||
./scripts/validate-ios-logs.sh device.log
|
||||
|
||||
# From live stream
|
||||
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"' | ./scripts/validate-ios-logs.sh
|
||||
|
||||
# From filtered grep
|
||||
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh
|
||||
```
|
||||
|
||||
**See:** `scripts/validate-ios-logs.sh` for complete validation script
|
||||
|
||||
---
|
||||
|
||||
## Common Log Prefixes
|
||||
|
||||
**Plugin Logs (look for these):**
|
||||
|
||||
| Prefix | Meaning | Example |
|
||||
|--------|---------|---------|
|
||||
| `[DNP-PLUGIN]` | Main plugin operations | `[DNP-PLUGIN] configure() called` |
|
||||
| `[DNP-FETCH]` | Background fetch operations | `[DNP-FETCH] BGTask handler invoked` |
|
||||
| `[DNP-SCHEDULER]` | Notification scheduling | `[DNP-SCHEDULER] Scheduling notification` |
|
||||
| `[DNP-STORAGE]` | Storage/DB operations | `[DNP-STORAGE] Persisted schedule` |
|
||||
| `[DNP-DEBUG]` | Debug diagnostics | `[DNP-DEBUG] Plugin class found` |
|
||||
|
||||
**Error Indicators:**
|
||||
|
||||
- `Error:` - System errors
|
||||
- `Failed:` - Operation failures
|
||||
- `❌` - Visual error markers in logs
|
||||
- `⚠️` - Warning markers
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
### Issue: No logs appearing
|
||||
|
||||
**Solutions:**
|
||||
1. **Check app is running:** App must be launched to generate logs
|
||||
2. **Check filter:** Remove filters to see all logs
|
||||
3. **Check log level:** Some logs may be at debug level only
|
||||
4. **Restart logging:** Close and reopen Console.app or restart log stream
|
||||
|
||||
### Issue: Too many logs (noise)
|
||||
|
||||
**Solutions:**
|
||||
1. **Use specific filters:** `DNP-` instead of `DailyNotification`
|
||||
2. **Filter by subsystem:** `subsystem:com.timesafari.dailynotification`
|
||||
3. **Use time range:** Only show logs from last 5 minutes
|
||||
4. **Use validation script:** Automatically filters for important events
|
||||
|
||||
### Issue: Can't see background task logs
|
||||
|
||||
**Solutions:**
|
||||
1. **Use Console.app:** Background tasks show better in system console
|
||||
2. **Check Background App Refresh:** Must be enabled for BGTask logs
|
||||
3. **Use log stream:** `xcrun simctl spawn booted log stream` shows all logs
|
||||
4. **Check predicate:** Use `--predicate` to filter specific subsystems
|
||||
|
||||
### Issue: Physical device logs not showing
|
||||
|
||||
**Solutions:**
|
||||
1. **Use Console.app:** Easiest for physical devices
|
||||
2. **Check device connection:** Device must be connected and trusted
|
||||
3. **Check provisioning:** Device must be provisioned for development
|
||||
4. **Use Xcode:** Xcode → Window → Devices and Simulators → View Device Logs
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Commands
|
||||
|
||||
### Copy-Paste Ready Commands:
|
||||
|
||||
```bash
|
||||
# Stream plugin logs (simulator)
|
||||
xcrun simctl spawn booted log stream --predicate 'subsystem == "com.timesafari.dailynotification"'
|
||||
|
||||
# Save logs to file
|
||||
xcrun simctl spawn booted log stream > device.log 2>&1
|
||||
|
||||
# View recent errors
|
||||
xcrun simctl spawn booted log show --last 5m | grep -i "error\|failed\|DNP-"
|
||||
|
||||
# Validate log sequence
|
||||
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh
|
||||
|
||||
# Check app logs only
|
||||
xcrun simctl spawn booted log stream --predicate 'process == "App"'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Levels and Filtering
|
||||
|
||||
**iOS Log Levels:**
|
||||
- **Default:** Shows Info, Error, Fault
|
||||
- **Debug:** Shows Debug, Info, Error, Fault
|
||||
- **Error:** Shows Error, Fault only
|
||||
|
||||
**To see debug logs:**
|
||||
- In Xcode: Product → Scheme → Edit Scheme → Run → Arguments → Environment Variables
|
||||
- Add: `OS_ACTIVITY_MODE=disable` (shows all logs including debug)
|
||||
|
||||
**Or use Console.app:**
|
||||
- Action menu → Include Info Messages
|
||||
- Action menu → Include Debug Messages
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Testing Guide:** `doc/test-app-ios/IOS_PREFETCH_TESTING.md` - Comprehensive testing procedures
|
||||
- **Test App Requirements:** `doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md` - Debugging section
|
||||
- **Validation Script:** `scripts/validate-ios-logs.sh` - Automated log sequence validation
|
||||
- **Main Directive:** `doc/directives/0003-iOS-Android-Parity-Directive.md` - Implementation details
|
||||
|
||||
---
|
||||
|
||||
**Status:** 🎯 **READY FOR USE**
|
||||
**Maintainer:** Matthew Raymer
|
||||
|
||||
@@ -3,9 +3,10 @@ import Capacitor
|
||||
import BackgroundTasks
|
||||
import DailyNotificationPlugin
|
||||
import ObjectiveC
|
||||
import UserNotifications
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
@@ -103,10 +104,65 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// NOTE: Background task registration is handled by DailyNotificationPlugin.load()
|
||||
// Do NOT register here to avoid duplicate registration crash
|
||||
|
||||
// Set notification center delegate to show notifications in foreground
|
||||
// Set immediately and also after Capacitor initializes to ensure it's always set
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
NSLog("DNP-DEBUG: UNUserNotificationCenter delegate set to AppDelegate (immediate)")
|
||||
|
||||
// Also set after Capacitor initializes (in case it resets the delegate)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
NSLog("DNP-DEBUG: UNUserNotificationCenter delegate re-set after Capacitor init")
|
||||
}
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
|
||||
// Re-set delegate when app becomes active (in case Capacitor resets it)
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
NSLog("DNP-DEBUG: UNUserNotificationCenter delegate re-set in applicationDidBecomeActive")
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
|
||||
/**
|
||||
* Show notifications even when app is in foreground
|
||||
* This is required for test app to see notifications during testing
|
||||
*/
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
NSLog("DNP-DEBUG: ✅ userNotificationCenter willPresent called!")
|
||||
NSLog("DNP-DEBUG: Notification received in foreground: %@", notification.request.identifier)
|
||||
NSLog("DNP-DEBUG: Notification title: %@", notification.request.content.title)
|
||||
NSLog("DNP-DEBUG: Notification body: %@", notification.request.content.body)
|
||||
NSLog("DNP-DEBUG: Current delegate: %@", UNUserNotificationCenter.current().delegate != nil ? "SET" : "NOT SET")
|
||||
|
||||
// Show notification with banner, sound, and badge
|
||||
// Use .banner for iOS 14+, fallback to .alert for iOS 13
|
||||
if #available(iOS 14.0, *) {
|
||||
completionHandler([.banner, .sound, .badge])
|
||||
} else {
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
NSLog("DNP-DEBUG: ✅ Completion handler called with presentation options")
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle notification tap/interaction
|
||||
*/
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
NSLog("DNP-DEBUG: Notification tapped: %@", response.notification.request.identifier)
|
||||
NSLog("DNP-DEBUG: Action identifier: %@", response.actionIdentifier)
|
||||
|
||||
// Handle notification tap if needed
|
||||
// For test app, we just log it
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||
@@ -121,10 +177,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@
|
||||
<div id="pluginStatusContent" style="margin-top: 8px;">
|
||||
Loading plugin status...
|
||||
</div>
|
||||
<div id="notificationReceivedIndicator" style="margin-top: 8px; padding: 8px; background: rgba(0, 255, 0, 0.2); border-radius: 5px; display: none;">
|
||||
<strong>🔔 Notification Received!</strong><br>
|
||||
<span id="notificationReceivedTime"></span><br>
|
||||
<small>Check the top of your screen for the notification banner</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -232,7 +237,8 @@
|
||||
const notificationTimeReadable = notificationTime.toLocaleTimeString();
|
||||
status.innerHTML = '✅ Notification scheduled!<br>' +
|
||||
'📥 Prefetch: ' + prefetchTimeReadable + ' (' + prefetchTimeString + ')<br>' +
|
||||
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')';
|
||||
'🔔 Notification: ' + notificationTimeReadable + ' (' + notificationTimeString + ')<br><br>' +
|
||||
'<small>💡 When the notification fires, look for a banner at the <strong>top of your screen</strong>.</small>';
|
||||
status.style.background = 'rgba(0, 255, 0, 0.3)'; // Green background
|
||||
// Refresh plugin status display
|
||||
setTimeout(() => loadPluginStatus(), 500);
|
||||
@@ -411,6 +417,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Check for notification delivery periodically
|
||||
function checkNotificationDelivery() {
|
||||
if (!window.DailyNotification) return;
|
||||
|
||||
window.DailyNotification.getNotificationStatus()
|
||||
.then(result => {
|
||||
if (result.lastNotificationTime) {
|
||||
const lastTime = new Date(result.lastNotificationTime);
|
||||
const now = new Date();
|
||||
const timeDiff = now - lastTime;
|
||||
|
||||
// If notification was received in the last 2 minutes, show indicator
|
||||
if (timeDiff > 0 && timeDiff < 120000) {
|
||||
const indicator = document.getElementById('notificationReceivedIndicator');
|
||||
const timeSpan = document.getElementById('notificationReceivedTime');
|
||||
|
||||
if (indicator && timeSpan) {
|
||||
indicator.style.display = 'block';
|
||||
timeSpan.textContent = `Received at ${lastTime.toLocaleTimeString()}`;
|
||||
|
||||
// Hide after 30 seconds
|
||||
setTimeout(() => {
|
||||
indicator.style.display = 'none';
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// Silently fail - this is just for visual feedback
|
||||
});
|
||||
}
|
||||
|
||||
// Load plugin status automatically on page load
|
||||
window.addEventListener('load', () => {
|
||||
console.log('Page loaded, loading plugin status...');
|
||||
@@ -419,6 +458,9 @@
|
||||
loadPluginStatus();
|
||||
loadPermissionStatus();
|
||||
loadChannelStatus();
|
||||
|
||||
// Check for notification delivery every 5 seconds
|
||||
setInterval(checkNotificationDelivery, 5000);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user