Compare commits
2 Commits
master
...
ios-implem
| Author | SHA1 | Date |
|---|---|---|
|
|
8ded555a21 | 1 day ago |
|
|
4be87acc14 | 2 days ago |
28 changed files with 4309 additions and 288 deletions
@ -0,0 +1,291 @@ |
|||||
|
# Building Everything from Console |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: November 4, 2025 |
||||
|
|
||||
|
## Quick Start |
||||
|
|
||||
|
Build everything (plugin + iOS + Android): |
||||
|
|
||||
|
```bash |
||||
|
./scripts/build-all.sh |
||||
|
``` |
||||
|
|
||||
|
Build specific platform: |
||||
|
|
||||
|
```bash |
||||
|
./scripts/build-all.sh ios # iOS only |
||||
|
./scripts/build-all.sh android # Android only |
||||
|
./scripts/build-all.sh all # Everything (default) |
||||
|
``` |
||||
|
|
||||
|
## What Gets Built |
||||
|
|
||||
|
### 1. Plugin Build |
||||
|
- Compiles TypeScript to JavaScript |
||||
|
- Builds native iOS code (Swift) |
||||
|
- Builds native Android code (Kotlin/Java) |
||||
|
- Creates plugin frameworks/bundles |
||||
|
|
||||
|
### 2. Android Build |
||||
|
- Builds Android app (`android/app`) |
||||
|
- Creates debug APK |
||||
|
- Output: `android/app/build/outputs/apk/debug/app-debug.apk` |
||||
|
|
||||
|
### 3. iOS Build |
||||
|
- Installs CocoaPods dependencies |
||||
|
- Builds iOS app (`ios/App`) |
||||
|
- Creates simulator app bundle |
||||
|
- Output: `ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app` |
||||
|
|
||||
|
## Detailed Build Process |
||||
|
|
||||
|
### Step-by-Step Build |
||||
|
|
||||
|
```bash |
||||
|
# 1. Build plugin (TypeScript + Native) |
||||
|
./scripts/build-native.sh --platform all |
||||
|
|
||||
|
# 2. Build Android app |
||||
|
cd android |
||||
|
./gradlew :app:assembleDebug |
||||
|
cd .. |
||||
|
|
||||
|
# 3. Build iOS app |
||||
|
cd ios |
||||
|
pod install |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'generic/platform=iOS Simulator' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
``` |
||||
|
|
||||
|
### Platform-Specific Builds |
||||
|
|
||||
|
#### Android Only |
||||
|
|
||||
|
```bash |
||||
|
# Build plugin for Android |
||||
|
./scripts/build-native.sh --platform android |
||||
|
|
||||
|
# Build Android app |
||||
|
cd android |
||||
|
./gradlew :app:assembleDebug |
||||
|
|
||||
|
# Install on device/emulator |
||||
|
adb install app/build/outputs/apk/debug/app-debug.apk |
||||
|
``` |
||||
|
|
||||
|
#### iOS Only |
||||
|
|
||||
|
```bash |
||||
|
# Build plugin for iOS |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
|
||||
|
# Install CocoaPods dependencies |
||||
|
cd ios |
||||
|
pod install |
||||
|
|
||||
|
# Build iOS app |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
|
||||
|
# Deploy to simulator (see deployment scripts) |
||||
|
../scripts/build-and-deploy-native-ios.sh |
||||
|
``` |
||||
|
|
||||
|
## Build Scripts |
||||
|
|
||||
|
### Main Build Script |
||||
|
|
||||
|
**`scripts/build-all.sh`** |
||||
|
- Builds plugin + iOS + Android |
||||
|
- Handles dependencies automatically |
||||
|
- Provides clear error messages |
||||
|
|
||||
|
### Platform-Specific Scripts |
||||
|
|
||||
|
**`scripts/build-native.sh`** |
||||
|
- Builds plugin only (TypeScript + native code) |
||||
|
- Supports `--platform ios`, `--platform android`, `--platform all` |
||||
|
|
||||
|
**`scripts/build-and-deploy-native-ios.sh`** |
||||
|
- Builds iOS plugin + app |
||||
|
- Deploys to simulator automatically |
||||
|
- Includes booting simulator and launching app |
||||
|
|
||||
|
**`test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh`** |
||||
|
- Builds Vue 3 test app |
||||
|
- Syncs web assets |
||||
|
- Deploys to simulator |
||||
|
|
||||
|
## Build Outputs |
||||
|
|
||||
|
### Android |
||||
|
|
||||
|
``` |
||||
|
android/app/build/outputs/apk/debug/app-debug.apk |
||||
|
``` |
||||
|
|
||||
|
### iOS |
||||
|
|
||||
|
``` |
||||
|
ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app |
||||
|
``` |
||||
|
|
||||
|
### Plugin |
||||
|
|
||||
|
``` |
||||
|
ios/build/derivedData/Build/Products/*/DailyNotificationPlugin.framework |
||||
|
android/plugin/build/outputs/aar/plugin-release.aar |
||||
|
``` |
||||
|
|
||||
|
## Prerequisites |
||||
|
|
||||
|
### For All Platforms |
||||
|
|
||||
|
- Node.js and npm |
||||
|
- Git |
||||
|
|
||||
|
### For Android |
||||
|
|
||||
|
- Android SDK |
||||
|
- Java JDK (8 or higher) |
||||
|
- Gradle (or use Gradle wrapper) |
||||
|
|
||||
|
### For iOS |
||||
|
|
||||
|
- macOS |
||||
|
- Xcode Command Line Tools |
||||
|
- CocoaPods (`gem install cocoapods`) |
||||
|
|
||||
|
## Troubleshooting |
||||
|
|
||||
|
### Build Fails |
||||
|
|
||||
|
```bash |
||||
|
# Clean and rebuild |
||||
|
./scripts/build-native.sh --platform all --clean |
||||
|
|
||||
|
# Android: Clean Gradle cache |
||||
|
cd android && ./gradlew clean && cd .. |
||||
|
|
||||
|
# iOS: Clean Xcode build |
||||
|
cd ios/App && xcodebuild clean && cd ../.. |
||||
|
``` |
||||
|
|
||||
|
### Dependencies Out of Date |
||||
|
|
||||
|
```bash |
||||
|
# Update npm dependencies |
||||
|
npm install |
||||
|
|
||||
|
# Update CocoaPods |
||||
|
cd ios && pod update && cd .. |
||||
|
|
||||
|
# Update Android dependencies |
||||
|
cd android && ./gradlew --refresh-dependencies && cd .. |
||||
|
``` |
||||
|
|
||||
|
### iOS Project Not Found |
||||
|
|
||||
|
If `ios/App/App.xcworkspace` doesn't exist: |
||||
|
|
||||
|
```bash |
||||
|
# Initialize iOS app with Capacitor |
||||
|
cd ios |
||||
|
npx cap sync ios |
||||
|
pod install |
||||
|
``` |
||||
|
|
||||
|
### Android Build Issues |
||||
|
|
||||
|
```bash |
||||
|
# Verify Android SDK |
||||
|
echo $ANDROID_HOME |
||||
|
|
||||
|
# Clean build |
||||
|
cd android |
||||
|
./gradlew clean |
||||
|
./gradlew :app:assembleDebug |
||||
|
``` |
||||
|
|
||||
|
## CI/CD Integration |
||||
|
|
||||
|
### GitHub Actions Example |
||||
|
|
||||
|
```yaml |
||||
|
name: Build All Platforms |
||||
|
|
||||
|
on: [push, pull_request] |
||||
|
|
||||
|
jobs: |
||||
|
build: |
||||
|
runs-on: macos-latest |
||||
|
steps: |
||||
|
- uses: actions/checkout@v2 |
||||
|
- uses: actions/setup-node@v2 |
||||
|
- name: Build Everything |
||||
|
run: ./scripts/build-all.sh all |
||||
|
``` |
||||
|
|
||||
|
### Android-Only CI |
||||
|
|
||||
|
```yaml |
||||
|
name: Build Android |
||||
|
|
||||
|
on: [push, pull_request] |
||||
|
|
||||
|
jobs: |
||||
|
build: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- uses: actions/checkout@v2 |
||||
|
- uses: actions/setup-node@v2 |
||||
|
- uses: actions/setup-java@v2 |
||||
|
- name: Build Android |
||||
|
run: ./scripts/build-all.sh android |
||||
|
``` |
||||
|
|
||||
|
## Verification |
||||
|
|
||||
|
After building, verify outputs: |
||||
|
|
||||
|
```bash |
||||
|
# Android APK exists |
||||
|
test -f android/app/build/outputs/apk/debug/app-debug.apk && echo "✓ Android APK" |
||||
|
|
||||
|
# iOS app bundle exists |
||||
|
test -d ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app && echo "✓ iOS app" |
||||
|
|
||||
|
# Plugin frameworks exist |
||||
|
test -d ios/build/derivedData/Build/Products/*/DailyNotificationPlugin.framework && echo "✓ iOS plugin" |
||||
|
test -f android/plugin/build/outputs/aar/plugin-release.aar && echo "✓ Android plugin" |
||||
|
``` |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
After building: |
||||
|
|
||||
|
1. **Deploy Android**: `adb install android/app/build/outputs/apk/debug/app-debug.apk` |
||||
|
2. **Deploy iOS**: Use `scripts/build-and-deploy-native-ios.sh` |
||||
|
3. **Test**: Run plugin tests and verify functionality |
||||
|
4. **Debug**: Use platform-specific debugging tools |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [Build Native Script](scripts/build-native.sh) |
||||
|
- [iOS Deployment Guide](docs/standalone-ios-simulator-guide.md) |
||||
|
- [Android Build Guide](BUILDING.md) |
||||
|
|
||||
@ -0,0 +1,199 @@ |
|||||
|
# Viewing Build Errors - Full Output Guide |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: November 4, 2025 |
||||
|
|
||||
|
## Quick Methods to See Full Errors |
||||
|
|
||||
|
### Method 1: Run Build Script and Check Log Files |
||||
|
|
||||
|
```bash |
||||
|
# Run the build script |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
|
||||
|
# If it fails, check the log files: |
||||
|
cat /tmp/xcodebuild_device.log # Device build errors |
||||
|
cat /tmp/xcodebuild_simulator.log # Simulator build errors |
||||
|
|
||||
|
# View only errors: |
||||
|
grep -E "(error:|warning:)" /tmp/xcodebuild_simulator.log |
||||
|
``` |
||||
|
|
||||
|
### Method 2: Run xcodebuild Directly (No Script Filtering) |
||||
|
|
||||
|
```bash |
||||
|
# Build for simulator with full output |
||||
|
cd ios |
||||
|
xcodebuild -workspace DailyNotificationPlugin.xcworkspace \ |
||||
|
-scheme DailyNotificationPlugin \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'generic/platform=iOS Simulator' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
| tee build-output.log |
||||
|
|
||||
|
# Then view errors: |
||||
|
grep -E "(error:|warning:)" build-output.log |
||||
|
``` |
||||
|
|
||||
|
### Method 3: Redirect to File and View |
||||
|
|
||||
|
```bash |
||||
|
# Save full output to file |
||||
|
./scripts/build-native.sh --platform ios 2>&1 | tee build-full.log |
||||
|
|
||||
|
# View errors |
||||
|
grep -E "(error:|warning:|ERROR|FAILED)" build-full.log |
||||
|
|
||||
|
# View last 100 lines |
||||
|
tail -100 build-full.log |
||||
|
``` |
||||
|
|
||||
|
### Method 4: Use xcodebuild with Verbose Output |
||||
|
|
||||
|
```bash |
||||
|
cd ios |
||||
|
|
||||
|
# Build with verbose output |
||||
|
xcodebuild -workspace DailyNotificationPlugin.xcworkspace \ |
||||
|
-scheme DailyNotificationPlugin \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'generic/platform=iOS Simulator' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
-verbose \ |
||||
|
2>&1 | tee build-verbose.log |
||||
|
|
||||
|
# Extract just errors |
||||
|
grep -E "^error:" build-verbose.log | head -50 |
||||
|
``` |
||||
|
|
||||
|
## Filtering Output |
||||
|
|
||||
|
### Show Only Errors |
||||
|
|
||||
|
```bash |
||||
|
# From log file |
||||
|
grep -E "^error:" /tmp/xcodebuild_simulator.log |
||||
|
|
||||
|
# From live output |
||||
|
./scripts/build-native.sh --platform ios 2>&1 | grep -E "(error:|ERROR)" |
||||
|
``` |
||||
|
|
||||
|
### Show Errors with Context (5 lines before/after) |
||||
|
|
||||
|
```bash |
||||
|
grep -E "(error:|warning:)" -A 5 -B 5 /tmp/xcodebuild_simulator.log | head -100 |
||||
|
``` |
||||
|
|
||||
|
### Count Errors |
||||
|
|
||||
|
```bash |
||||
|
grep -c "error:" /tmp/xcodebuild_simulator.log |
||||
|
``` |
||||
|
|
||||
|
### Show Errors Grouped by File |
||||
|
|
||||
|
```bash |
||||
|
grep "error:" /tmp/xcodebuild_simulator.log | cut -d: -f1-3 | sort | uniq -c | sort -rn |
||||
|
``` |
||||
|
|
||||
|
## Common Error Patterns |
||||
|
|
||||
|
### Swift Compilation Errors |
||||
|
|
||||
|
```bash |
||||
|
# Find all Swift compilation errors |
||||
|
grep -E "\.swift.*error:" /tmp/xcodebuild_simulator.log |
||||
|
|
||||
|
# Find missing type errors |
||||
|
grep -E "cannot find type.*in scope" /tmp/xcodebuild_simulator.log |
||||
|
|
||||
|
# Find import errors |
||||
|
grep -E "No such module|Cannot find.*in scope" /tmp/xcodebuild_simulator.log |
||||
|
``` |
||||
|
|
||||
|
### CocoaPods Errors |
||||
|
|
||||
|
```bash |
||||
|
# Find CocoaPods errors |
||||
|
grep -E "(pod|CocoaPods)" /tmp/xcodebuild_simulator.log -i |
||||
|
``` |
||||
|
|
||||
|
### Build System Errors |
||||
|
|
||||
|
```bash |
||||
|
# Find build system errors |
||||
|
grep -E "(BUILD FAILED|error:)" /tmp/xcodebuild_simulator.log |
||||
|
``` |
||||
|
|
||||
|
## Debugging Tips |
||||
|
|
||||
|
### See Full Command Being Run |
||||
|
|
||||
|
Add `set -x` at the top of the script, or run: |
||||
|
|
||||
|
```bash |
||||
|
bash -x ./scripts/build-native.sh --platform ios 2>&1 | tee build-debug.log |
||||
|
``` |
||||
|
|
||||
|
### Check Exit Codes |
||||
|
|
||||
|
```bash |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
echo "Exit code: $?" |
||||
|
``` |
||||
|
|
||||
|
### View Build Settings |
||||
|
|
||||
|
```bash |
||||
|
cd ios |
||||
|
xcodebuild -workspace DailyNotificationPlugin.xcworkspace \ |
||||
|
-scheme DailyNotificationPlugin \ |
||||
|
-showBuildSettings 2>&1 | grep -E "(SWIFT|FRAMEWORK|HEADER)" |
||||
|
``` |
||||
|
|
||||
|
## Example: Full Debug Session |
||||
|
|
||||
|
```bash |
||||
|
# 1. Run build and save everything |
||||
|
./scripts/build-native.sh --platform ios 2>&1 | tee build-full.log |
||||
|
|
||||
|
# 2. Check exit code |
||||
|
echo "Build exit code: $?" |
||||
|
|
||||
|
# 3. Extract errors |
||||
|
echo "=== ERRORS ===" > errors.txt |
||||
|
grep -E "(error:|ERROR)" build-full.log >> errors.txt |
||||
|
|
||||
|
# 4. Extract warnings |
||||
|
echo "=== WARNINGS ===" >> errors.txt |
||||
|
grep -E "(warning:|WARNING)" build-full.log >> errors.txt |
||||
|
|
||||
|
# 5. View errors file |
||||
|
cat errors.txt |
||||
|
|
||||
|
# 6. Check log files created by script |
||||
|
ls -lh /tmp/xcodebuild*.log |
||||
|
``` |
||||
|
|
||||
|
## Quick Reference |
||||
|
|
||||
|
```bash |
||||
|
# Most common: View simulator build errors |
||||
|
cat /tmp/xcodebuild_simulator.log | grep -E "(error:|warning:)" | head -30 |
||||
|
|
||||
|
# View full build log |
||||
|
cat /tmp/xcodebuild_simulator.log | less |
||||
|
|
||||
|
# Search for specific error |
||||
|
grep -i "cannot find type" /tmp/xcodebuild_simulator.log |
||||
|
|
||||
|
# Count errors by type |
||||
|
grep "error:" /tmp/xcodebuild_simulator.log | cut -d: -f4 | sort | uniq -c | sort -rn |
||||
|
``` |
||||
|
|
||||
@ -0,0 +1,64 @@ |
|||||
|
# Web Assets Structure - Android and iOS Parity |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: November 4, 2025 |
||||
|
|
||||
|
## Source of Truth |
||||
|
|
||||
|
The **`www/`** directory is the source of truth for web assets. Both Android and iOS app directories should match this structure. |
||||
|
|
||||
|
## Directory Structure |
||||
|
|
||||
|
``` |
||||
|
www/ # Source of truth (edit here) |
||||
|
├── index.html # Main test interface |
||||
|
|
||||
|
android/app/src/main/assets/ # Android (synced from www/) |
||||
|
├── capacitor.plugins.json # Auto-generated by Capacitor |
||||
|
└── public/ # Web assets (must match www/) |
||||
|
└── index.html # Synced from www/index.html |
||||
|
|
||||
|
ios/App/App/ # iOS (synced from www/) |
||||
|
├── capacitor.config.json # Capacitor configuration |
||||
|
└── public/ # Web assets (must match www/) |
||||
|
└── index.html # Synced from www/index.html |
||||
|
``` |
||||
|
|
||||
|
## Synchronization |
||||
|
|
||||
|
Both `android/app/src/main/assets/public/` and `ios/App/App/public/` should match `www/` after running: |
||||
|
|
||||
|
```bash |
||||
|
# Sync web assets to both platforms |
||||
|
npx cap sync |
||||
|
|
||||
|
# Or sync individually |
||||
|
npx cap sync android |
||||
|
npx cap sync ios |
||||
|
``` |
||||
|
|
||||
|
## Key Points |
||||
|
|
||||
|
1. **Edit source files in `www/`** - Never edit platform-specific copies directly |
||||
|
2. **Both platforms should match** - After sync, `android/.../assets/public/` and `ios/App/App/public/` should be identical |
||||
|
3. **Capacitor handles sync** - `npx cap sync` copies files from `www/` to platform directories |
||||
|
4. **Auto-generated files** - `capacitor.plugins.json`, `capacitor.js`, etc. are generated by Capacitor |
||||
|
|
||||
|
## Verification |
||||
|
|
||||
|
After syncing, verify both platforms match: |
||||
|
|
||||
|
```bash |
||||
|
# Check file sizes match |
||||
|
ls -lh www/index.html android/app/src/main/assets/public/index.html ios/App/App/public/index.html |
||||
|
|
||||
|
# Compare contents |
||||
|
diff www/index.html android/app/src/main/assets/public/index.html |
||||
|
diff www/index.html ios/App/App/public/index.html |
||||
|
``` |
||||
|
|
||||
|
## Notes |
||||
|
|
||||
|
- **Cordova files**: iOS may have empty `cordova.js` and `cordova_plugins.js` files. These are harmless but should be removed if not using Cordova compatibility. |
||||
|
- **Capacitor runtime**: Capacitor generates `capacitor.js` and `capacitor_plugins.js` during sync - these are auto-generated and should not be manually edited. |
||||
|
|
||||
@ -0,0 +1,185 @@ |
|||||
|
# iOS Native Interface Structure |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: November 4, 2025 |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
The iOS native interface mirrors the Android structure, providing the same functionality through iOS-specific implementations. |
||||
|
|
||||
|
## Directory Structure |
||||
|
|
||||
|
``` |
||||
|
ios/App/App/ |
||||
|
├── AppDelegate.swift # Application lifecycle (equivalent to PluginApplication.java) |
||||
|
├── ViewController.swift # Main view controller (equivalent to MainActivity.java) |
||||
|
├── SceneDelegate.swift # Scene-based lifecycle (iOS 13+) |
||||
|
├── Info.plist # App configuration (equivalent to AndroidManifest.xml) |
||||
|
├── capacitor.config.json # Capacitor configuration |
||||
|
├── config.xml # Cordova compatibility |
||||
|
└── public/ # Web assets (equivalent to assets/public/) |
||||
|
├── index.html |
||||
|
├── capacitor.js |
||||
|
└── capacitor_plugins.js |
||||
|
``` |
||||
|
|
||||
|
## File Descriptions |
||||
|
|
||||
|
### AppDelegate.swift |
||||
|
|
||||
|
**Purpose**: Application lifecycle management |
||||
|
**Equivalent**: `PluginApplication.java` on Android |
||||
|
|
||||
|
- Handles app lifecycle events (launch, background, foreground, termination) |
||||
|
- Registers for push notifications |
||||
|
- Handles URL schemes and universal links |
||||
|
- Initializes plugin demo fetcher (equivalent to Android's `PluginApplication.onCreate()`) |
||||
|
|
||||
|
**Key Methods**: |
||||
|
- `application(_:didFinishLaunchingWithOptions:)` - App initialization |
||||
|
- `applicationDidEnterBackground(_:)` - Background handling |
||||
|
- `applicationWillEnterForeground(_:)` - Foreground handling |
||||
|
- `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)` - Push notification registration |
||||
|
|
||||
|
### ViewController.swift |
||||
|
|
||||
|
**Purpose**: Main view controller extending Capacitor's bridge |
||||
|
**Equivalent**: `MainActivity.java` on Android |
||||
|
|
||||
|
- Extends `CAPBridgeViewController` (Capacitor's bridge view controller) |
||||
|
- Initializes plugin and registers native fetcher |
||||
|
- Handles view lifecycle events |
||||
|
|
||||
|
**Key Methods**: |
||||
|
- `viewDidLoad()` - View initialization |
||||
|
- `initializePlugin()` - Plugin registration (equivalent to Android's plugin registration) |
||||
|
|
||||
|
### SceneDelegate.swift |
||||
|
|
||||
|
**Purpose**: Scene-based lifecycle management (iOS 13+) |
||||
|
**Equivalent**: None on Android (iOS-specific) |
||||
|
|
||||
|
- Handles scene creation and lifecycle |
||||
|
- Manages window and view controller setup |
||||
|
- Required for modern iOS apps using scene-based architecture |
||||
|
|
||||
|
### Info.plist |
||||
|
|
||||
|
**Purpose**: App configuration and permissions |
||||
|
**Equivalent**: `AndroidManifest.xml` on Android |
||||
|
|
||||
|
**Key Entries**: |
||||
|
- `CFBundleIdentifier` - App bundle ID |
||||
|
- `NSUserNotificationsUsageDescription` - Notification permission description |
||||
|
- `UIBackgroundModes` - Background modes (fetch, processing, remote-notification) |
||||
|
- `BGTaskSchedulerPermittedIdentifiers` - Background task identifiers |
||||
|
- `UIApplicationSceneManifest` - Scene configuration |
||||
|
|
||||
|
## Comparison: Android vs iOS |
||||
|
|
||||
|
| Component | Android | iOS | |
||||
|
|-----------|---------|-----| |
||||
|
| **Application Class** | `PluginApplication.java` | `AppDelegate.swift` | |
||||
|
| **Main Activity** | `MainActivity.java` | `ViewController.swift` | |
||||
|
| **Config File** | `AndroidManifest.xml` | `Info.plist` | |
||||
|
| **Web Assets** | `assets/public/` | `public/` | |
||||
|
| **Lifecycle** | `onCreate()`, `onResume()`, etc. | `viewDidLoad()`, `viewWillAppear()`, etc. | |
||||
|
| **Bridge** | `BridgeActivity` | `CAPBridgeViewController` | |
||||
|
|
||||
|
## Plugin Registration |
||||
|
|
||||
|
### Android |
||||
|
|
||||
|
```java |
||||
|
public class PluginApplication extends Application { |
||||
|
@Override |
||||
|
public void onCreate() { |
||||
|
super.onCreate(); |
||||
|
NativeNotificationContentFetcher demoFetcher = new DemoNativeFetcher(); |
||||
|
DailyNotificationPlugin.setNativeFetcher(demoFetcher); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### iOS |
||||
|
|
||||
|
```swift |
||||
|
class AppDelegate: UIResponder, UIApplicationDelegate { |
||||
|
func application(_ application: UIApplication, |
||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { |
||||
|
// Plugin registration happens in ViewController after Capacitor bridge is initialized |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class ViewController: CAPBridgeViewController { |
||||
|
override func viewDidLoad() { |
||||
|
super.viewDidLoad() |
||||
|
initializePlugin() |
||||
|
} |
||||
|
|
||||
|
private func initializePlugin() { |
||||
|
// Register demo native fetcher if implementing SPI |
||||
|
// DailyNotificationPlugin.setNativeFetcher(DemoNativeFetcher()) |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Build Process |
||||
|
|
||||
|
1. **Swift Compilation**: Compiles `AppDelegate.swift`, `ViewController.swift`, `SceneDelegate.swift` |
||||
|
2. **Capacitor Integration**: Links with Capacitor framework and plugin |
||||
|
3. **Web Assets**: Copies `public/` directory to app bundle |
||||
|
4. **Info.plist**: Processes app configuration and permissions |
||||
|
5. **App Bundle**: Creates `.app` bundle for installation |
||||
|
|
||||
|
## Permissions |
||||
|
|
||||
|
### Android (AndroidManifest.xml) |
||||
|
|
||||
|
```xml |
||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> |
||||
|
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> |
||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> |
||||
|
``` |
||||
|
|
||||
|
### iOS (Info.plist) |
||||
|
|
||||
|
```xml |
||||
|
<key>NSUserNotificationsUsageDescription</key> |
||||
|
<string>This app uses notifications to deliver daily updates and reminders.</string> |
||||
|
|
||||
|
<key>UIBackgroundModes</key> |
||||
|
<array> |
||||
|
<string>background-fetch</string> |
||||
|
<string>background-processing</string> |
||||
|
<string>remote-notification</string> |
||||
|
</array> |
||||
|
``` |
||||
|
|
||||
|
## Background Tasks |
||||
|
|
||||
|
### Android |
||||
|
|
||||
|
- Uses `WorkManager` and `AlarmManager` |
||||
|
- Declared in `AndroidManifest.xml` receivers |
||||
|
|
||||
|
### iOS |
||||
|
|
||||
|
- Uses `BGTaskScheduler` and `UNUserNotificationCenter` |
||||
|
- Declared in `Info.plist` with `BGTaskSchedulerPermittedIdentifiers` |
||||
|
|
||||
|
## Next Steps |
||||
|
|
||||
|
1. Ensure Xcode project includes these Swift files |
||||
|
2. Configure build settings in Xcode project |
||||
|
3. Add app icons and launch screen |
||||
|
4. Test plugin registration and native fetcher |
||||
|
5. Verify background tasks work correctly |
||||
|
|
||||
|
## References |
||||
|
|
||||
|
- [Capacitor iOS Documentation](https://capacitorjs.com/docs/ios) |
||||
|
- [iOS App Lifecycle](https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle) |
||||
|
- [Background Tasks](https://developer.apple.com/documentation/backgroundtasks) |
||||
|
|
||||
@ -0,0 +1,548 @@ |
|||||
|
# Running iOS Apps in Standalone Simulator (Without Xcode UI) |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Last Updated**: 2025-11-04 |
||||
|
**Version**: 1.0.0 |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This guide demonstrates how to run DailyNotification plugin test apps in a standalone iOS Simulator without using Xcode UI. This method is useful for development, CI/CD pipelines, and command-line workflows. |
||||
|
|
||||
|
**There are two different test apps:** |
||||
|
1. **Native iOS Development App** (`ios/App`) - Simple Capacitor app for plugin development |
||||
|
2. **Vue 3 Test App** (`test-apps/daily-notification-test`) - Full-featured Vue 3 Capacitor app for comprehensive testing |
||||
|
|
||||
|
## Prerequisites |
||||
|
|
||||
|
### Required Software |
||||
|
- **Xcode** with command line tools (`xcode-select --install`) |
||||
|
- **iOS Simulator** (included with Xcode) |
||||
|
- **CocoaPods** (`gem install cocoapods`) |
||||
|
- **Node.js** and **npm** (for TypeScript compilation) |
||||
|
- **Capacitor CLI** (`npm install -g @capacitor/cli`) - for Vue 3 test app |
||||
|
|
||||
|
### System Requirements |
||||
|
- **macOS** (required for iOS development) |
||||
|
- **RAM**: 4GB minimum, 8GB recommended |
||||
|
- **Storage**: 5GB free space for simulator and dependencies |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Scenario 1: Native iOS Development App (`ios/App`) |
||||
|
|
||||
|
The `ios/App` directory contains a simple Capacitor-based development app, similar to `android/app`. This is used for quick plugin testing and development. |
||||
|
|
||||
|
### Step-by-Step Process |
||||
|
|
||||
|
#### 1. Check Available Simulators |
||||
|
|
||||
|
```bash |
||||
|
# List available iOS simulators |
||||
|
xcrun simctl list devices available |
||||
|
|
||||
|
# Example output: |
||||
|
# iPhone 15 Pro (iOS 17.0) |
||||
|
# iPhone 14 (iOS 16.4) |
||||
|
# iPad Pro (12.9-inch) (iOS 17.0) |
||||
|
``` |
||||
|
|
||||
|
#### 2. Boot a Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Boot a specific simulator device |
||||
|
xcrun simctl boot "iPhone 15 Pro" |
||||
|
|
||||
|
# Or boot by device ID |
||||
|
xcrun simctl boot <DEVICE_ID> |
||||
|
|
||||
|
# Verify simulator is running |
||||
|
xcrun simctl list devices | grep Booted |
||||
|
``` |
||||
|
|
||||
|
**Alternative: Open Simulator UI** |
||||
|
```bash |
||||
|
# Open Simulator app (allows visual interaction) |
||||
|
open -a Simulator |
||||
|
``` |
||||
|
|
||||
|
#### 3. Build the Plugin |
||||
|
|
||||
|
```bash |
||||
|
# Navigate to project directory |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
|
||||
|
# Build plugin for iOS |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
``` |
||||
|
|
||||
|
**What this does:** |
||||
|
- Compiles TypeScript to JavaScript |
||||
|
- Builds iOS native code (Swift) |
||||
|
- Creates plugin framework |
||||
|
- Builds for simulator |
||||
|
|
||||
|
#### 4. Build Native iOS Development App |
||||
|
|
||||
|
```bash |
||||
|
# Navigate to iOS directory |
||||
|
cd ios |
||||
|
|
||||
|
# Install CocoaPods dependencies |
||||
|
pod install |
||||
|
|
||||
|
# Build the development app for simulator |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
-derivedDataPath build/derivedData \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
clean build |
||||
|
``` |
||||
|
|
||||
|
#### 5. Install App on Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Find the built app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
||||
|
|
||||
|
# Install app on simulator |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
``` |
||||
|
|
||||
|
#### 6. Launch the App |
||||
|
|
||||
|
```bash |
||||
|
# Get bundle identifier from Info.plist |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
|
||||
|
# Launch the app |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
|
||||
|
# Example: |
||||
|
# xcrun simctl launch booted com.timesafari.dailynotification |
||||
|
``` |
||||
|
|
||||
|
#### 7. Monitor App Logs |
||||
|
|
||||
|
```bash |
||||
|
# View all logs |
||||
|
xcrun simctl spawn booted log stream |
||||
|
|
||||
|
# Filter for specific processes |
||||
|
xcrun simctl spawn booted log stream --predicate 'processImagePath contains "App"' |
||||
|
|
||||
|
# View system logs |
||||
|
log stream --predicate 'processImagePath contains "App"' --level debug |
||||
|
``` |
||||
|
|
||||
|
### Complete Command Sequence for Native iOS App |
||||
|
|
||||
|
```bash |
||||
|
# 1. Boot simulator |
||||
|
xcrun simctl boot "iPhone 15 Pro" || open -a Simulator |
||||
|
|
||||
|
# 2. Build plugin |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
|
||||
|
# 3. Build native iOS app |
||||
|
cd ios |
||||
|
pod install |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
|
||||
|
# 4. Install app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
|
||||
|
# 5. Launch app |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Scenario 2: Vue 3 Test App (`test-apps/daily-notification-test`) |
||||
|
|
||||
|
The `test-apps/daily-notification-test` directory contains a full-featured Vue 3 Capacitor app with comprehensive testing interface, similar to the Android test app. |
||||
|
|
||||
|
### Step-by-Step Process |
||||
|
|
||||
|
#### 1. Check Available Simulators |
||||
|
|
||||
|
```bash |
||||
|
# List available iOS simulators |
||||
|
xcrun simctl list devices available |
||||
|
``` |
||||
|
|
||||
|
#### 2. Boot a Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Boot a specific simulator device |
||||
|
xcrun simctl boot "iPhone 15 Pro" |
||||
|
|
||||
|
# Or open Simulator UI |
||||
|
open -a Simulator |
||||
|
``` |
||||
|
|
||||
|
#### 3. Build the Plugin |
||||
|
|
||||
|
```bash |
||||
|
# Navigate to project directory |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
|
||||
|
# Build plugin for iOS |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
``` |
||||
|
|
||||
|
#### 4. Set Up Vue 3 Test App iOS Project |
||||
|
|
||||
|
```bash |
||||
|
# Navigate to test app |
||||
|
cd test-apps/daily-notification-test |
||||
|
|
||||
|
# Install dependencies |
||||
|
npm install |
||||
|
|
||||
|
# Add iOS platform (if not already added) |
||||
|
npx cap add ios |
||||
|
|
||||
|
# Sync web assets with iOS project |
||||
|
npx cap sync ios |
||||
|
``` |
||||
|
|
||||
|
#### 5. Build Vue 3 Test App |
||||
|
|
||||
|
```bash |
||||
|
# Build web assets (Vue 3 app) |
||||
|
npm run build |
||||
|
|
||||
|
# Sync with iOS project |
||||
|
npx cap sync ios |
||||
|
|
||||
|
# Build iOS app for simulator |
||||
|
cd ios/App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
-derivedDataPath build/derivedData \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
``` |
||||
|
|
||||
|
#### 6. Install App on Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Find the built app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
||||
|
|
||||
|
# Install app on simulator |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
``` |
||||
|
|
||||
|
#### 7. Launch the App |
||||
|
|
||||
|
```bash |
||||
|
# Get bundle identifier |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
|
||||
|
# Launch the app |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
|
||||
|
# Example: |
||||
|
# xcrun simctl launch booted com.timesafari.dailynotification.test |
||||
|
``` |
||||
|
|
||||
|
### Complete Command Sequence for Vue 3 Test App |
||||
|
|
||||
|
```bash |
||||
|
# 1. Boot simulator |
||||
|
xcrun simctl boot "iPhone 15 Pro" || open -a Simulator |
||||
|
|
||||
|
# 2. Build plugin |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
|
||||
|
# 3. Set up Vue 3 test app |
||||
|
cd test-apps/daily-notification-test |
||||
|
npm install |
||||
|
npm run build |
||||
|
|
||||
|
# 4. Sync with iOS |
||||
|
npx cap sync ios |
||||
|
|
||||
|
# 5. Build iOS app |
||||
|
cd ios/App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
|
||||
|
# 6. Install app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
|
||||
|
# 7. Launch app |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Alternative Methods |
||||
|
|
||||
|
### Method 1: Using Capacitor CLI (Vue 3 Test App Only) |
||||
|
|
||||
|
```bash |
||||
|
cd test-apps/daily-notification-test |
||||
|
|
||||
|
# Build and run in one command |
||||
|
npx cap run ios |
||||
|
|
||||
|
# This will: |
||||
|
# - Build the plugin |
||||
|
# - Sync web assets |
||||
|
# - Build and install app |
||||
|
# - Launch in simulator |
||||
|
``` |
||||
|
|
||||
|
**Note**: This method only works for the Vue 3 test app, not the native iOS development app. |
||||
|
|
||||
|
### Method 2: Using Automated Scripts |
||||
|
|
||||
|
#### For Native iOS App (`ios/App`) |
||||
|
|
||||
|
Create `scripts/build-and-deploy-native-ios.sh`: |
||||
|
|
||||
|
```bash |
||||
|
#!/bin/bash |
||||
|
set -e |
||||
|
|
||||
|
echo "🔨 Building plugin..." |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
|
||||
|
echo "📱 Booting simulator..." |
||||
|
xcrun simctl boot "iPhone 15 Pro" 2>/dev/null || open -a Simulator |
||||
|
|
||||
|
echo "🏗️ Building native iOS app..." |
||||
|
cd ios |
||||
|
pod install |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
|
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
``` |
||||
|
|
||||
|
#### For Vue 3 Test App |
||||
|
|
||||
|
Use the existing script: |
||||
|
```bash |
||||
|
cd test-apps/daily-notification-test |
||||
|
./scripts/build-and-deploy-ios.sh |
||||
|
``` |
||||
|
|
||||
|
### Method 3: Manual Xcode Build |
||||
|
|
||||
|
```bash |
||||
|
# Open project in Xcode |
||||
|
open ios/App/App.xcworkspace # For native app |
||||
|
# OR |
||||
|
open test-apps/daily-notification-test/ios/App/App.xcworkspace # For Vue 3 app |
||||
|
|
||||
|
# Then: |
||||
|
# 1. Select simulator target |
||||
|
# 2. Click Run button (⌘R) |
||||
|
# 3. App builds and launches automatically |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Simulator Management |
||||
|
|
||||
|
### List Available Devices |
||||
|
|
||||
|
```bash |
||||
|
# List all devices |
||||
|
xcrun simctl list devices |
||||
|
|
||||
|
# List only available devices |
||||
|
xcrun simctl list devices available |
||||
|
|
||||
|
# List booted devices |
||||
|
xcrun simctl list devices | grep Booted |
||||
|
``` |
||||
|
|
||||
|
### Shutdown Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Shutdown specific device |
||||
|
xcrun simctl shutdown "iPhone 15 Pro" |
||||
|
|
||||
|
# Shutdown all devices |
||||
|
xcrun simctl shutdown all |
||||
|
``` |
||||
|
|
||||
|
### Reset Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Erase all content and settings |
||||
|
xcrun simctl erase "iPhone 15 Pro" |
||||
|
|
||||
|
# Reset and boot |
||||
|
xcrun simctl erase "iPhone 15 Pro" && xcrun simctl boot "iPhone 15 Pro" |
||||
|
``` |
||||
|
|
||||
|
### Delete App from Simulator |
||||
|
|
||||
|
```bash |
||||
|
# Uninstall app (replace with correct bundle ID) |
||||
|
xcrun simctl uninstall booted com.timesafari.dailynotification |
||||
|
# OR for Vue 3 test app: |
||||
|
xcrun simctl uninstall booted com.timesafari.dailynotification.test |
||||
|
|
||||
|
# Or reset entire simulator |
||||
|
xcrun simctl erase booted |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Comparison: Native iOS App vs Vue 3 Test App |
||||
|
|
||||
|
| Feature | Native iOS App (`ios/App`) | Vue 3 Test App (`test-apps/...`) | |
||||
|
|---------|---------------------------|----------------------------------| |
||||
|
| **Purpose** | Quick plugin development testing | Comprehensive testing with UI | |
||||
|
| **Frontend** | Simple HTML/Capacitor | Vue 3 with full UI | |
||||
|
| **Build Steps** | Plugin + iOS build | Plugin + Vue build + iOS build | |
||||
|
| **Capacitor Sync** | Not required | Required (`npx cap sync ios`) | |
||||
|
| **Best For** | Quick native testing | Full integration testing | |
||||
|
| **Bundle ID** | `com.timesafari.dailynotification` | `com.timesafari.dailynotification.test` | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Comparison with Android |
||||
|
|
||||
|
| Task | Android Native App | iOS Native App | Vue 3 Test App (iOS) | |
||||
|
|------|-------------------|----------------|---------------------| |
||||
|
| List devices | `emulator -list-avds` | `xcrun simctl list devices` | `xcrun simctl list devices` | |
||||
|
| Boot device | `emulator -avd <name>` | `xcrun simctl boot <name>` | `xcrun simctl boot <name>` | |
||||
|
| Install app | `adb install <apk>` | `xcrun simctl install booted <app>` | `xcrun simctl install booted <app>` | |
||||
|
| Launch app | `adb shell am start` | `xcrun simctl launch booted <bundle>` | `xcrun simctl launch booted <bundle>` | |
||||
|
| View logs | `adb logcat` | `xcrun simctl spawn booted log stream` | `xcrun simctl spawn booted log stream` | |
||||
|
| Build command | `./gradlew assembleDebug` | `xcodebuild -workspace ...` | `xcodebuild -workspace ...` | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Troubleshooting |
||||
|
|
||||
|
### Simulator Won't Boot |
||||
|
|
||||
|
```bash |
||||
|
# Check Xcode command line tools |
||||
|
xcode-select -p |
||||
|
|
||||
|
# Reinstall command line tools |
||||
|
sudo xcode-select --reset |
||||
|
|
||||
|
# Verify simulator runtime |
||||
|
xcrun simctl runtime list |
||||
|
``` |
||||
|
|
||||
|
### Build Fails |
||||
|
|
||||
|
```bash |
||||
|
# Clean build folder |
||||
|
cd ios/App # or ios/App for Vue 3 app |
||||
|
xcodebuild clean -workspace App.xcworkspace -scheme App |
||||
|
|
||||
|
# Reinstall CocoaPods dependencies |
||||
|
cd ../.. # Back to ios/ directory |
||||
|
pod install --repo-update |
||||
|
|
||||
|
# Rebuild |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug |
||||
|
``` |
||||
|
|
||||
|
### App Won't Install |
||||
|
|
||||
|
```bash |
||||
|
# Check if simulator is booted |
||||
|
xcrun simctl list devices | grep Booted |
||||
|
|
||||
|
# Verify app path exists |
||||
|
ls -la ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/ |
||||
|
|
||||
|
# Check bundle identifier |
||||
|
plutil -extract CFBundleIdentifier raw ios/App/App/Info.plist |
||||
|
``` |
||||
|
|
||||
|
### Vue 3 App: Web Assets Not Syncing |
||||
|
|
||||
|
```bash |
||||
|
# Rebuild web assets |
||||
|
cd test-apps/daily-notification-test |
||||
|
npm run build |
||||
|
|
||||
|
# Force sync |
||||
|
npx cap sync ios --force |
||||
|
|
||||
|
# Verify assets are synced |
||||
|
ls -la ios/App/App/public/ |
||||
|
``` |
||||
|
|
||||
|
### Logs Not Showing |
||||
|
|
||||
|
```bash |
||||
|
# Use Console.app for better log viewing |
||||
|
open -a Console |
||||
|
|
||||
|
# Or use log command with filters |
||||
|
log stream --predicate 'processImagePath contains "App"' --level debug --style compact |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Additional Resources |
||||
|
|
||||
|
- [Capacitor iOS Documentation](https://capacitorjs.com/docs/ios) |
||||
|
- [Xcode Command Line Tools](https://developer.apple.com/xcode/resources/) |
||||
|
- [Simulator Documentation](https://developer.apple.com/documentation/xcode/running-your-app-in-the-simulator-or-on-a-device) |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Note**: iOS development requires macOS. This guide assumes you're running on a Mac with Xcode installed. |
||||
|
|
||||
|
**Key Distinction**: |
||||
|
- **`ios/App`** = Native iOS development app (simple, for quick testing) |
||||
|
- **`test-apps/daily-notification-test`** = Vue 3 test app (full-featured, for comprehensive testing) |
||||
|
|
||||
@ -0,0 +1,106 @@ |
|||||
|
// |
||||
|
// AppDelegate.swift |
||||
|
// DailyNotification Test App |
||||
|
// |
||||
|
// Application delegate for the Daily Notification Plugin demo app. |
||||
|
// Registers the native content fetcher SPI implementation. |
||||
|
// |
||||
|
// @author Matthew Raymer |
||||
|
// @version 1.0.0 |
||||
|
// @created 2025-11-04 |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
import Capacitor |
||||
|
|
||||
|
/** |
||||
|
* Application delegate for Daily Notification Plugin demo app |
||||
|
* Equivalent to PluginApplication.java on Android |
||||
|
*/ |
||||
|
@main |
||||
|
class AppDelegate: UIResponder, UIApplicationDelegate { |
||||
|
|
||||
|
var window: UIWindow? |
||||
|
|
||||
|
func application( |
||||
|
_ application: UIApplication, |
||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? |
||||
|
) -> Bool { |
||||
|
// Initialize Daily Notification Plugin demo fetcher |
||||
|
// Note: This is called before Capacitor bridge is initialized |
||||
|
// Plugin registration happens in ViewController |
||||
|
|
||||
|
print("AppDelegate: Initializing Daily Notification Plugin demo app") |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func applicationWillResignActive(_ application: UIApplication) { |
||||
|
// Pause ongoing tasks |
||||
|
} |
||||
|
|
||||
|
func applicationDidEnterBackground(_ application: UIApplication) { |
||||
|
// Release resources when app enters background |
||||
|
} |
||||
|
|
||||
|
func applicationWillEnterForeground(_ application: UIApplication) { |
||||
|
// Restore resources when app enters foreground |
||||
|
} |
||||
|
|
||||
|
func applicationDidBecomeActive(_ application: UIApplication) { |
||||
|
// Restart paused tasks |
||||
|
} |
||||
|
|
||||
|
func applicationWillTerminate(_ application: UIApplication) { |
||||
|
// Save data before app terminates |
||||
|
} |
||||
|
|
||||
|
// MARK: - URL Scheme Handling |
||||
|
|
||||
|
func application( |
||||
|
_ app: UIApplication, |
||||
|
open url: URL, |
||||
|
options: [UIApplication.OpenURLOptionsKey: Any] = [:] |
||||
|
) -> Bool { |
||||
|
// Handle URL schemes (e.g., deep links) |
||||
|
return ApplicationDelegateProxy.shared.application(app, open: url, options: options) |
||||
|
} |
||||
|
|
||||
|
// MARK: - Universal Links |
||||
|
|
||||
|
func application( |
||||
|
_ application: UIApplication, |
||||
|
continue userActivity: NSUserActivity, |
||||
|
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void |
||||
|
) -> Bool { |
||||
|
// Handle universal links |
||||
|
return ApplicationDelegateProxy.shared.application( |
||||
|
application, |
||||
|
continue: userActivity, |
||||
|
restorationHandler: restorationHandler |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// MARK: - Push Notifications |
||||
|
|
||||
|
func application( |
||||
|
_ application: UIApplication, |
||||
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data |
||||
|
) { |
||||
|
// Handle device token registration |
||||
|
NotificationCenter.default.post( |
||||
|
name: Notification.Name("didRegisterForRemoteNotifications"), |
||||
|
object: nil, |
||||
|
userInfo: ["deviceToken": deviceToken] |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
func application( |
||||
|
_ application: UIApplication, |
||||
|
didFailToRegisterForRemoteNotificationsWithError error: Error |
||||
|
) { |
||||
|
// Handle registration failure |
||||
|
print("AppDelegate: Failed to register for remote notifications: \(error)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,119 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
|
<plist version="1.0"> |
||||
|
<dict> |
||||
|
<!-- App Display Name --> |
||||
|
<key>CFBundleDisplayName</key> |
||||
|
<string>DailyNotification Test</string> |
||||
|
|
||||
|
<!-- Bundle Identifier --> |
||||
|
<key>CFBundleIdentifier</key> |
||||
|
<string>com.timesafari.dailynotification</string> |
||||
|
|
||||
|
<!-- Bundle Name --> |
||||
|
<key>CFBundleName</key> |
||||
|
<string>DailyNotification Test App</string> |
||||
|
|
||||
|
<!-- Version --> |
||||
|
<key>CFBundleShortVersionString</key> |
||||
|
<string>1.0.0</string> |
||||
|
|
||||
|
<!-- Build Number --> |
||||
|
<key>CFBundleVersion</key> |
||||
|
<string>1</string> |
||||
|
|
||||
|
<!-- Minimum iOS Version --> |
||||
|
<key>LSMinimumSystemVersion</key> |
||||
|
<string>13.0</string> |
||||
|
|
||||
|
<!-- Device Family --> |
||||
|
<key>UIDeviceFamily</key> |
||||
|
<array> |
||||
|
<integer>1</integer> |
||||
|
<integer>2</integer> |
||||
|
</array> |
||||
|
|
||||
|
<!-- Supported Interface Orientations --> |
||||
|
<key>UISupportedInterfaceOrientations</key> |
||||
|
<array> |
||||
|
<string>UIInterfaceOrientationPortrait</string> |
||||
|
<string>UIInterfaceOrientationLandscapeLeft</string> |
||||
|
<string>UIInterfaceOrientationLandscapeRight</string> |
||||
|
</array> |
||||
|
|
||||
|
<!-- Supported Interface Orientations (iPad) --> |
||||
|
<key>UISupportedInterfaceOrientations~ipad</key> |
||||
|
<array> |
||||
|
<string>UIInterfaceOrientationPortrait</string> |
||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string> |
||||
|
<string>UIInterfaceOrientationLandscapeLeft</string> |
||||
|
<string>UIInterfaceOrientationLandscapeRight</string> |
||||
|
</array> |
||||
|
|
||||
|
<!-- Status Bar Style --> |
||||
|
<key>UIStatusBarStyle</key> |
||||
|
<string>UIStatusBarStyleDefault</string> |
||||
|
|
||||
|
<!-- Status Bar Hidden --> |
||||
|
<key>UIStatusBarHidden</key> |
||||
|
<false/> |
||||
|
|
||||
|
<!-- Launch Screen --> |
||||
|
<key>UILaunchStoryboardName</key> |
||||
|
<string>LaunchScreen</string> |
||||
|
|
||||
|
<!-- Privacy Usage Descriptions --> |
||||
|
<key>NSUserNotificationsUsageDescription</key> |
||||
|
<string>This app uses notifications to deliver daily updates and reminders.</string> |
||||
|
|
||||
|
<!-- Background Modes --> |
||||
|
<key>UIBackgroundModes</key> |
||||
|
<array> |
||||
|
<string>background-fetch</string> |
||||
|
<string>background-processing</string> |
||||
|
<string>remote-notification</string> |
||||
|
</array> |
||||
|
|
||||
|
<!-- Background Task Identifiers --> |
||||
|
<key>BGTaskSchedulerPermittedIdentifiers</key> |
||||
|
<array> |
||||
|
<string>com.timesafari.dailynotification.fetch</string> |
||||
|
<string>com.timesafari.dailynotification.notify</string> |
||||
|
</array> |
||||
|
|
||||
|
<!-- App Transport Security --> |
||||
|
<key>NSAppTransportSecurity</key> |
||||
|
<dict> |
||||
|
<key>NSAllowsArbitraryLoads</key> |
||||
|
<false/> |
||||
|
<key>NSExceptionDomains</key> |
||||
|
<dict> |
||||
|
<!-- Add your callback domains here --> |
||||
|
</dict> |
||||
|
</dict> |
||||
|
|
||||
|
<!-- Scene Configuration (iOS 13+) --> |
||||
|
<key>UIApplicationSceneManifest</key> |
||||
|
<dict> |
||||
|
<key>UIApplicationSupportsMultipleScenes</key> |
||||
|
<false/> |
||||
|
<key>UISceneConfigurations</key> |
||||
|
<dict> |
||||
|
<key>UIWindowSceneSessionRoleApplication</key> |
||||
|
<array> |
||||
|
<dict> |
||||
|
<key>UISceneConfigurationName</key> |
||||
|
<string>Default Configuration</string> |
||||
|
<key>UISceneDelegateClassName</key> |
||||
|
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> |
||||
|
</dict> |
||||
|
</array> |
||||
|
</dict> |
||||
|
</dict> |
||||
|
|
||||
|
<!-- Background App Refresh --> |
||||
|
<key>UIApplicationExitsOnSuspend</key> |
||||
|
<false/> |
||||
|
</dict> |
||||
|
</plist> |
||||
|
|
||||
@ -0,0 +1,61 @@ |
|||||
|
// |
||||
|
// SceneDelegate.swift |
||||
|
// DailyNotification Test App |
||||
|
// |
||||
|
// Scene delegate for iOS 13+ scene-based lifecycle. |
||||
|
// Handles scene creation and lifecycle events. |
||||
|
// |
||||
|
// @author Matthew Raymer |
||||
|
// @version 1.0.0 |
||||
|
// @created 2025-11-04 |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
/** |
||||
|
* Scene delegate for iOS 13+ scene-based lifecycle |
||||
|
* Required for modern iOS apps using scene-based architecture |
||||
|
*/ |
||||
|
@available(iOS 13.0, *) |
||||
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate { |
||||
|
|
||||
|
var window: UIWindow? |
||||
|
|
||||
|
func scene( |
||||
|
_ scene: UIScene, |
||||
|
willConnectTo session: UISceneSession, |
||||
|
options connectionOptions: UIScene.ConnectionOptions |
||||
|
) { |
||||
|
// Called when a new scene session is being created |
||||
|
guard let windowScene = (scene as? UIWindowScene) else { return } |
||||
|
|
||||
|
let window = UIWindow(windowScene: windowScene) |
||||
|
self.window = window |
||||
|
|
||||
|
// Create and configure the view controller |
||||
|
let viewController = ViewController() |
||||
|
window.rootViewController = viewController |
||||
|
window.makeKeyAndVisible() |
||||
|
} |
||||
|
|
||||
|
func sceneDidDisconnect(_ scene: UIScene) { |
||||
|
// Called when the scene is being released by the system |
||||
|
} |
||||
|
|
||||
|
func sceneDidBecomeActive(_ scene: UIScene) { |
||||
|
// Called when the scene has moved from inactive to active state |
||||
|
} |
||||
|
|
||||
|
func sceneWillResignActive(_ scene: UIScene) { |
||||
|
// Called when the scene will move from active to inactive state |
||||
|
} |
||||
|
|
||||
|
func sceneWillEnterForeground(_ scene: UIScene) { |
||||
|
// Called when the scene is about to move from background to foreground |
||||
|
} |
||||
|
|
||||
|
func sceneDidEnterBackground(_ scene: UIScene) { |
||||
|
// Called when the scene has moved from background to foreground |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,69 @@ |
|||||
|
// |
||||
|
// ViewController.swift |
||||
|
// DailyNotification Test App |
||||
|
// |
||||
|
// Main view controller for the Daily Notification Plugin demo app. |
||||
|
// Equivalent to MainActivity.java on Android - extends Capacitor's bridge. |
||||
|
// |
||||
|
// @author Matthew Raymer |
||||
|
// @version 1.0.0 |
||||
|
// @created 2025-11-04 |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
import Capacitor |
||||
|
|
||||
|
/** |
||||
|
* Main view controller extending Capacitor's bridge view controller |
||||
|
* Equivalent to MainActivity extends BridgeActivity on Android |
||||
|
*/ |
||||
|
class ViewController: CAPBridgeViewController { |
||||
|
|
||||
|
override func viewDidLoad() { |
||||
|
super.viewDidLoad() |
||||
|
|
||||
|
// Initialize Daily Notification Plugin demo fetcher |
||||
|
// This is called after Capacitor bridge is initialized |
||||
|
initializePlugin() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initialize plugin and register native fetcher |
||||
|
* Equivalent to PluginApplication.onCreate() on Android |
||||
|
*/ |
||||
|
private func initializePlugin() { |
||||
|
print("ViewController: Initializing Daily Notification Plugin") |
||||
|
|
||||
|
// Note: Plugin registration happens automatically via Capacitor |
||||
|
// Native fetcher registration can be done here if needed |
||||
|
|
||||
|
// Example: Register demo native fetcher (if implementing SPI) |
||||
|
// DailyNotificationPlugin.setNativeFetcher(DemoNativeFetcher()) |
||||
|
|
||||
|
print("ViewController: Daily Notification Plugin initialized") |
||||
|
} |
||||
|
|
||||
|
override func viewWillAppear(_ animated: Bool) { |
||||
|
super.viewWillAppear(animated) |
||||
|
} |
||||
|
|
||||
|
override func viewDidAppear(_ animated: Bool) { |
||||
|
super.viewDidAppear(animated) |
||||
|
} |
||||
|
|
||||
|
override func viewWillDisappear(_ animated: Bool) { |
||||
|
super.viewWillDisappear(animated) |
||||
|
} |
||||
|
|
||||
|
override func viewDidDisappear(_ animated: Bool) { |
||||
|
super.viewDidDisappear(animated) |
||||
|
} |
||||
|
|
||||
|
// MARK: - Memory Management |
||||
|
|
||||
|
override func didReceiveMemoryWarning() { |
||||
|
super.didReceiveMemoryWarning() |
||||
|
// Dispose of any resources that can be recreated |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,412 @@ |
|||||
|
/** |
||||
|
* DailyNotificationStorage.swift |
||||
|
* |
||||
|
* Storage management for notification content and settings |
||||
|
* Implements tiered storage: Key-Value (quick) + DB (structured) + Files (large assets) |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @version 1.0.0 |
||||
|
*/ |
||||
|
|
||||
|
import Foundation |
||||
|
|
||||
|
/** |
||||
|
* Manages storage for notification content and settings |
||||
|
* |
||||
|
* This class implements the tiered storage approach: |
||||
|
* - Tier 1: UserDefaults for quick access to settings and recent data |
||||
|
* - Tier 2: In-memory cache for structured notification content |
||||
|
* - Tier 3: File system for large assets (future use) |
||||
|
*/ |
||||
|
class DailyNotificationStorage { |
||||
|
|
||||
|
// MARK: - Constants |
||||
|
|
||||
|
private static let TAG = "DailyNotificationStorage" |
||||
|
private static let PREFS_NAME = "DailyNotificationPrefs" |
||||
|
private static let KEY_NOTIFICATIONS = "notifications" |
||||
|
private static let KEY_SETTINGS = "settings" |
||||
|
private static let KEY_LAST_FETCH = "last_fetch" |
||||
|
private static let KEY_ADAPTIVE_SCHEDULING = "adaptive_scheduling" |
||||
|
|
||||
|
private static let MAX_CACHE_SIZE = 100 // Maximum notifications to keep in memory |
||||
|
private static let CACHE_CLEANUP_INTERVAL: TimeInterval = 24 * 60 * 60 // 24 hours |
||||
|
private static let MAX_STORAGE_ENTRIES = 100 // Maximum total storage entries |
||||
|
private static let RETENTION_PERIOD_MS: TimeInterval = 14 * 24 * 60 * 60 * 1000 // 14 days |
||||
|
private static let BATCH_CLEANUP_SIZE = 50 // Clean up in batches |
||||
|
|
||||
|
// MARK: - Properties |
||||
|
|
||||
|
private let userDefaults: UserDefaults |
||||
|
private var notificationCache: [String: NotificationContent] = [:] |
||||
|
private var notificationList: [NotificationContent] = [] |
||||
|
private let storageQueue = DispatchQueue(label: "storage.queue", attributes: .concurrent) |
||||
|
private let logger: DailyNotificationLogger? |
||||
|
|
||||
|
// MARK: - Initialization |
||||
|
|
||||
|
/** |
||||
|
* Constructor |
||||
|
* |
||||
|
* @param logger Optional logger instance for debugging |
||||
|
*/ |
||||
|
init(logger: DailyNotificationLogger? = nil) { |
||||
|
self.userDefaults = UserDefaults(suiteName: Self.PREFS_NAME) ?? UserDefaults.standard |
||||
|
self.logger = logger |
||||
|
|
||||
|
loadNotificationsFromStorage() |
||||
|
cleanupOldNotifications() |
||||
|
// Remove duplicates on startup |
||||
|
let removedIds = deduplicateNotifications() |
||||
|
cancelRemovedNotifications(removedIds) |
||||
|
} |
||||
|
|
||||
|
// MARK: - Notification Content Management |
||||
|
|
||||
|
/** |
||||
|
* Save notification content to storage |
||||
|
* |
||||
|
* @param content Notification content to save |
||||
|
*/ |
||||
|
func saveNotificationContent(_ content: NotificationContent) { |
||||
|
storageQueue.async(flags: .barrier) { |
||||
|
self.logger?.log(.debug, "DN|STORAGE_SAVE_START id=\(content.id)") |
||||
|
|
||||
|
// Add to cache |
||||
|
self.notificationCache[content.id] = content |
||||
|
|
||||
|
// Add to list and sort by scheduled time |
||||
|
self.notificationList.removeAll { $0.id == content.id } |
||||
|
self.notificationList.append(content) |
||||
|
self.notificationList.sort { $0.scheduledTime < $1.scheduledTime } |
||||
|
|
||||
|
// Apply storage cap and retention policy |
||||
|
self.enforceStorageLimits() |
||||
|
|
||||
|
// Persist to UserDefaults |
||||
|
self.saveNotificationsToStorage() |
||||
|
|
||||
|
self.logger?.log(.debug, "DN|STORAGE_SAVE_OK id=\(content.id) total=\(self.notificationList.count)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get notification content by ID |
||||
|
* |
||||
|
* @param id Notification ID |
||||
|
* @return Notification content or nil if not found |
||||
|
*/ |
||||
|
func getNotificationContent(_ id: String) -> NotificationContent? { |
||||
|
return storageQueue.sync { |
||||
|
return notificationCache[id] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the last notification that was delivered |
||||
|
* |
||||
|
* @return Last notification or nil if none exists |
||||
|
*/ |
||||
|
func getLastNotification() -> NotificationContent? { |
||||
|
return storageQueue.sync { |
||||
|
if notificationList.isEmpty { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// Find the most recent delivered notification |
||||
|
let currentTime = Date().timeIntervalSince1970 * 1000 |
||||
|
for notification in notificationList.reversed() { |
||||
|
if notification.scheduledTime <= currentTime { |
||||
|
return notification |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get all notifications |
||||
|
* |
||||
|
* @return Array of all notifications |
||||
|
*/ |
||||
|
func getAllNotifications() -> [NotificationContent] { |
||||
|
return storageQueue.sync { |
||||
|
return Array(notificationList) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get notifications that are ready to be displayed |
||||
|
* |
||||
|
* @return Array of ready notifications |
||||
|
*/ |
||||
|
func getReadyNotifications() -> [NotificationContent] { |
||||
|
return storageQueue.sync { |
||||
|
let currentTime = Date().timeIntervalSince1970 * 1000 |
||||
|
return notificationList.filter { $0.scheduledTime <= currentTime } |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the next scheduled notification |
||||
|
* |
||||
|
* @return Next notification or nil if none scheduled |
||||
|
*/ |
||||
|
func getNextNotification() -> NotificationContent? { |
||||
|
return storageQueue.sync { |
||||
|
let currentTime = Date().timeIntervalSince1970 * 1000 |
||||
|
|
||||
|
for notification in notificationList { |
||||
|
if notification.scheduledTime > currentTime { |
||||
|
return notification |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Remove notification by ID |
||||
|
* |
||||
|
* @param id Notification ID to remove |
||||
|
*/ |
||||
|
func removeNotification(_ id: String) { |
||||
|
storageQueue.async(flags: .barrier) { |
||||
|
self.notificationCache.removeValue(forKey: id) |
||||
|
self.notificationList.removeAll { $0.id == id } |
||||
|
self.saveNotificationsToStorage() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clear all notifications |
||||
|
*/ |
||||
|
func clearAllNotifications() { |
||||
|
storageQueue.async(flags: .barrier) { |
||||
|
self.notificationCache.removeAll() |
||||
|
self.notificationList.removeAll() |
||||
|
self.saveNotificationsToStorage() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get notification count |
||||
|
* |
||||
|
* @return Number of notifications stored |
||||
|
*/ |
||||
|
func getNotificationCount() -> Int { |
||||
|
return storageQueue.sync { |
||||
|
return notificationList.count |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if storage is empty |
||||
|
* |
||||
|
* @return true if no notifications stored |
||||
|
*/ |
||||
|
func isEmpty() -> Bool { |
||||
|
return storageQueue.sync { |
||||
|
return notificationList.isEmpty |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Settings Management |
||||
|
|
||||
|
/** |
||||
|
* Set sound enabled setting |
||||
|
* |
||||
|
* @param enabled Whether sound is enabled |
||||
|
*/ |
||||
|
func setSoundEnabled(_ enabled: Bool) { |
||||
|
userDefaults.set(enabled, forKey: "sound_enabled") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if sound is enabled |
||||
|
* |
||||
|
* @return true if sound is enabled |
||||
|
*/ |
||||
|
func isSoundEnabled() -> Bool { |
||||
|
return userDefaults.bool(forKey: "sound_enabled") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set notification priority |
||||
|
* |
||||
|
* @param priority Priority level (e.g., "high", "normal", "low") |
||||
|
*/ |
||||
|
func setPriority(_ priority: String) { |
||||
|
userDefaults.set(priority, forKey: "priority") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get notification priority |
||||
|
* |
||||
|
* @return Priority level or "normal" if not set |
||||
|
*/ |
||||
|
func getPriority() -> String { |
||||
|
return userDefaults.string(forKey: "priority") ?? "normal" |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set timezone |
||||
|
* |
||||
|
* @param timezone Timezone identifier |
||||
|
*/ |
||||
|
func setTimezone(_ timezone: String) { |
||||
|
userDefaults.set(timezone, forKey: "timezone") |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get timezone |
||||
|
* |
||||
|
* @return Timezone identifier or system default |
||||
|
*/ |
||||
|
func getTimezone() -> String { |
||||
|
return userDefaults.string(forKey: "timezone") ?? TimeZone.current.identifier |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set adaptive scheduling enabled |
||||
|
* |
||||
|
* @param enabled Whether adaptive scheduling is enabled |
||||
|
*/ |
||||
|
func setAdaptiveSchedulingEnabled(_ enabled: Bool) { |
||||
|
userDefaults.set(enabled, forKey: Self.KEY_ADAPTIVE_SCHEDULING) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if adaptive scheduling is enabled |
||||
|
* |
||||
|
* @return true if adaptive scheduling is enabled |
||||
|
*/ |
||||
|
func isAdaptiveSchedulingEnabled() -> Bool { |
||||
|
return userDefaults.bool(forKey: Self.KEY_ADAPTIVE_SCHEDULING) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set last fetch time |
||||
|
* |
||||
|
* @param time Last fetch time in milliseconds since epoch |
||||
|
*/ |
||||
|
func setLastFetchTime(_ time: TimeInterval) { |
||||
|
userDefaults.set(time, forKey: Self.KEY_LAST_FETCH) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get last fetch time |
||||
|
* |
||||
|
* @return Last fetch time in milliseconds since epoch, or 0 if not set |
||||
|
*/ |
||||
|
func getLastFetchTime() -> TimeInterval { |
||||
|
return userDefaults.double(forKey: Self.KEY_LAST_FETCH) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if we should fetch new content |
||||
|
* |
||||
|
* @param minInterval Minimum interval between fetches in milliseconds |
||||
|
* @return true if enough time has passed since last fetch |
||||
|
*/ |
||||
|
func shouldFetchNewContent(minInterval: TimeInterval) -> Bool { |
||||
|
let lastFetch = getLastFetchTime() |
||||
|
if lastFetch == 0 { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
let currentTime = Date().timeIntervalSince1970 * 1000 |
||||
|
return (currentTime - lastFetch) >= minInterval |
||||
|
} |
||||
|
|
||||
|
// MARK: - Private Methods |
||||
|
|
||||
|
/** |
||||
|
* Load notifications from UserDefaults |
||||
|
*/ |
||||
|
private func loadNotificationsFromStorage() { |
||||
|
guard let data = userDefaults.data(forKey: Self.KEY_NOTIFICATIONS), |
||||
|
let jsonArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
notificationList = jsonArray.compactMap { NotificationContent.fromDictionary($0) } |
||||
|
notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) }) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Save notifications to UserDefaults |
||||
|
*/ |
||||
|
private func saveNotificationsToStorage() { |
||||
|
let jsonArray = notificationList.map { $0.toDictionary() } |
||||
|
|
||||
|
if let data = try? JSONSerialization.data(withJSONObject: jsonArray) { |
||||
|
userDefaults.set(data, forKey: Self.KEY_NOTIFICATIONS) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clean up old notifications based on retention policy |
||||
|
*/ |
||||
|
private func cleanupOldNotifications() { |
||||
|
let currentTime = Date().timeIntervalSince1970 * 1000 |
||||
|
let cutoffTime = currentTime - Self.RETENTION_PERIOD_MS |
||||
|
|
||||
|
notificationList.removeAll { notification in |
||||
|
let age = currentTime - notification.scheduledTime |
||||
|
return age > Self.RETENTION_PERIOD_MS |
||||
|
} |
||||
|
|
||||
|
// Update cache |
||||
|
notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) }) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Enforce storage limits |
||||
|
*/ |
||||
|
private func enforceStorageLimits() { |
||||
|
// Remove oldest notifications if over limit |
||||
|
while notificationList.count > Self.MAX_STORAGE_ENTRIES { |
||||
|
let oldest = notificationList.removeFirst() |
||||
|
notificationCache.removeValue(forKey: oldest.id) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Deduplicate notifications |
||||
|
* |
||||
|
* @return Array of removed notification IDs |
||||
|
*/ |
||||
|
private func deduplicateNotifications() -> [String] { |
||||
|
var seen = Set<String>() |
||||
|
var removed: [String] = [] |
||||
|
|
||||
|
notificationList = notificationList.filter { notification in |
||||
|
if seen.contains(notification.id) { |
||||
|
removed.append(notification.id) |
||||
|
return false |
||||
|
} |
||||
|
seen.insert(notification.id) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// Update cache |
||||
|
notificationCache = Dictionary(uniqueKeysWithValues: notificationList.map { ($0.id, $0) }) |
||||
|
|
||||
|
return removed |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Cancel removed notifications |
||||
|
* |
||||
|
* @param ids Array of notification IDs to cancel |
||||
|
*/ |
||||
|
private func cancelRemovedNotifications(_ ids: [String]) { |
||||
|
// This would typically cancel alarms/workers for these IDs |
||||
|
// Implementation depends on scheduler integration |
||||
|
logger?.log(.debug, "DN|STORAGE_DEDUP removed=\(ids.count)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,16 @@ |
|||||
|
#!/bin/bash |
||||
|
# Quick NVM setup script |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
echo "Installing NVM (Node Version Manager)..." |
||||
|
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash |
||||
|
|
||||
|
echo "" |
||||
|
echo "NVM installed! Please run:" |
||||
|
echo " source ~/.zshrc" |
||||
|
echo "" |
||||
|
echo "Then install Node.js with:" |
||||
|
echo " nvm install --lts" |
||||
|
echo " nvm use --lts" |
||||
|
echo " nvm alias default node" |
||||
@ -0,0 +1,245 @@ |
|||||
|
#!/bin/bash |
||||
|
# Complete Build Script - Build Everything from Console |
||||
|
# Builds plugin, iOS app, Android app, and all dependencies |
||||
|
# |
||||
|
# Usage: |
||||
|
# ./scripts/build-all.sh [platform] |
||||
|
# Platform options: ios, android, all (default: all) |
||||
|
# |
||||
|
# @author Matthew Raymer |
||||
|
# @version 1.0.0 |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Colors for output |
||||
|
RED='\033[0;31m' |
||||
|
GREEN='\033[0;32m' |
||||
|
YELLOW='\033[1;33m' |
||||
|
BLUE='\033[0;34m' |
||||
|
NC='\033[0m' # No Color |
||||
|
|
||||
|
log_info() { |
||||
|
echo -e "${GREEN}[INFO]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_warn() { |
||||
|
echo -e "${YELLOW}[WARN]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_error() { |
||||
|
echo -e "${RED}[ERROR]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_step() { |
||||
|
echo -e "${BLUE}[STEP]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
# Get script directory |
||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" |
||||
|
|
||||
|
# Parse arguments |
||||
|
PLATFORM="${1:-all}" |
||||
|
|
||||
|
# Validate platform |
||||
|
if [[ ! "$PLATFORM" =~ ^(ios|android|all)$ ]]; then |
||||
|
log_error "Invalid platform: $PLATFORM" |
||||
|
log_info "Usage: $0 [ios|android|all]" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
cd "$PROJECT_ROOT" |
||||
|
|
||||
|
log_info "==========================================" |
||||
|
log_info "Complete Build Script" |
||||
|
log_info "Platform: $PLATFORM" |
||||
|
log_info "==========================================" |
||||
|
log_info "" |
||||
|
|
||||
|
# Build TypeScript and plugin code |
||||
|
log_step "Building plugin (TypeScript + Native)..." |
||||
|
if ! ./scripts/build-native.sh --platform "$PLATFORM" 2>&1 | tee /tmp/build-native-output.log; then |
||||
|
log_error "Plugin build failed" |
||||
|
log_info "" |
||||
|
log_info "Full build output saved to: /tmp/build-native-output.log" |
||||
|
log_info "View errors: grep -E '(error:|ERROR|FAILED)' /tmp/build-native-output.log" |
||||
|
log_info "" |
||||
|
log_info "Checking for xcodebuild logs..." |
||||
|
if [ -f "/tmp/xcodebuild_device.log" ]; then |
||||
|
log_info "Device build errors:" |
||||
|
grep -E "(error:|warning:)" /tmp/xcodebuild_device.log | head -30 |
||||
|
fi |
||||
|
if [ -f "/tmp/xcodebuild_simulator.log" ]; then |
||||
|
log_info "Simulator build errors:" |
||||
|
grep -E "(error:|warning:)" /tmp/xcodebuild_simulator.log | head -30 |
||||
|
fi |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Build Android |
||||
|
if [[ "$PLATFORM" == "android" || "$PLATFORM" == "all" ]]; then |
||||
|
log_step "Building Android app..." |
||||
|
|
||||
|
cd "$PROJECT_ROOT/android" |
||||
|
|
||||
|
if [ ! -f "gradlew" ]; then |
||||
|
log_error "Gradle wrapper not found. Run: cd android && ./gradlew wrapper" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Build Android app |
||||
|
if ! ./gradlew :app:assembleDebug; then |
||||
|
log_error "Android build failed" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
APK_PATH="app/build/outputs/apk/debug/app-debug.apk" |
||||
|
if [ -f "$APK_PATH" ]; then |
||||
|
log_info "✓ Android APK: $APK_PATH" |
||||
|
else |
||||
|
log_error "Android APK not found at $APK_PATH" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
log_info "" |
||||
|
fi |
||||
|
|
||||
|
# Build iOS |
||||
|
if [[ "$PLATFORM" == "ios" || "$PLATFORM" == "all" ]]; then |
||||
|
log_step "Building iOS app..." |
||||
|
|
||||
|
cd "$PROJECT_ROOT/ios" |
||||
|
|
||||
|
# Check if CocoaPods is installed |
||||
|
if ! command -v pod &> /dev/null; then |
||||
|
log_error "CocoaPods not found. Install with: gem install cocoapods" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Install CocoaPods dependencies |
||||
|
log_step "Installing CocoaPods dependencies..." |
||||
|
if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ]; then |
||||
|
pod install |
||||
|
else |
||||
|
log_info "CocoaPods dependencies up to date" |
||||
|
fi |
||||
|
|
||||
|
# Check if App workspace exists |
||||
|
if [ ! -d "App/App.xcworkspace" ] && [ ! -d "App/App.xcodeproj" ]; then |
||||
|
log_warn "iOS app Xcode project not found" |
||||
|
log_info "The iOS app may need to be initialized with Capacitor" |
||||
|
log_info "Try: cd ios && npx cap sync ios" |
||||
|
log_info "" |
||||
|
log_info "Attempting to build plugin framework only..." |
||||
|
|
||||
|
# Build plugin framework only |
||||
|
cd "$PROJECT_ROOT/ios" |
||||
|
if [ -d "DailyNotificationPlugin.xcworkspace" ]; then |
||||
|
WORKSPACE="DailyNotificationPlugin.xcworkspace" |
||||
|
SCHEME="DailyNotificationPlugin" |
||||
|
CONFIG="Debug" |
||||
|
|
||||
|
log_step "Building plugin framework for simulator..." |
||||
|
xcodebuild build \ |
||||
|
-workspace "$WORKSPACE" \ |
||||
|
-scheme "$SCHEME" \ |
||||
|
-configuration "$CONFIG" \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'generic/platform=iOS Simulator' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO || log_warn "Plugin framework build failed (may need Xcode project setup)" |
||||
|
else |
||||
|
log_error "Cannot find iOS workspace or project" |
||||
|
exit 1 |
||||
|
fi |
||||
|
else |
||||
|
# Build iOS app |
||||
|
cd "$PROJECT_ROOT/ios/App" |
||||
|
|
||||
|
# Determine workspace vs project |
||||
|
if [ -d "App.xcworkspace" ]; then |
||||
|
WORKSPACE="App.xcworkspace" |
||||
|
BUILD_CMD="xcodebuild -workspace" |
||||
|
elif [ -d "App.xcodeproj" ]; then |
||||
|
PROJECT="App.xcodeproj" |
||||
|
BUILD_CMD="xcodebuild -project" |
||||
|
else |
||||
|
log_error "Cannot find iOS workspace or project" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
SCHEME="App" |
||||
|
CONFIG="Debug" |
||||
|
SDK="iphonesimulator" |
||||
|
|
||||
|
log_step "Building iOS app for simulator..." |
||||
|
|
||||
|
if [ -n "$WORKSPACE" ]; then |
||||
|
BUILD_OUTPUT=$(xcodebuild build \ |
||||
|
-workspace "$WORKSPACE" \ |
||||
|
-scheme "$SCHEME" \ |
||||
|
-configuration "$CONFIG" \ |
||||
|
-sdk "$SDK" \ |
||||
|
-destination 'generic/platform=iOS Simulator' \ |
||||
|
-derivedDataPath build/derivedData \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
2>&1) |
||||
|
else |
||||
|
BUILD_OUTPUT=$(xcodebuild build \ |
||||
|
-project "$PROJECT" \ |
||||
|
-scheme "$SCHEME" \ |
||||
|
-configuration "$CONFIG" \ |
||||
|
-sdk "$SDK" \ |
||||
|
-destination 'generic/platform=iOS Simulator' \ |
||||
|
-derivedDataPath build/derivedData \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
2>&1) |
||||
|
fi |
||||
|
|
||||
|
if echo "$BUILD_OUTPUT" | grep -q "BUILD SUCCEEDED"; then |
||||
|
log_info "✓ iOS app build completed successfully" |
||||
|
|
||||
|
# Find built app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
||||
|
if [ -n "$APP_PATH" ]; then |
||||
|
log_info "✓ iOS app bundle: $APP_PATH" |
||||
|
fi |
||||
|
elif echo "$BUILD_OUTPUT" | grep -q "error:"; then |
||||
|
log_error "iOS app build failed" |
||||
|
echo "$BUILD_OUTPUT" | grep -E "(error:|warning:)" | head -20 |
||||
|
exit 1 |
||||
|
else |
||||
|
log_warn "iOS app build completed with warnings" |
||||
|
echo "$BUILD_OUTPUT" | grep -E "(warning:|error:)" | head -10 |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
log_info "" |
||||
|
fi |
||||
|
|
||||
|
log_info "==========================================" |
||||
|
log_info "✅ Build Complete!" |
||||
|
log_info "==========================================" |
||||
|
log_info "" |
||||
|
|
||||
|
# Summary |
||||
|
if [[ "$PLATFORM" == "android" || "$PLATFORM" == "all" ]]; then |
||||
|
log_info "Android APK: android/app/build/outputs/apk/debug/app-debug.apk" |
||||
|
log_info "Install: adb install android/app/build/outputs/apk/debug/app-debug.apk" |
||||
|
fi |
||||
|
|
||||
|
if [[ "$PLATFORM" == "ios" || "$PLATFORM" == "all" ]]; then |
||||
|
log_info "iOS App: ios/App/build/derivedData/Build/Products/Debug-iphonesimulator/App.app" |
||||
|
log_info "Install: xcrun simctl install booted <APP_PATH>" |
||||
|
fi |
||||
|
|
||||
|
log_info "" |
||||
|
log_info "For deployment scripts, see:" |
||||
|
log_info " - scripts/build-and-deploy-native-ios.sh (iOS native app)" |
||||
|
log_info " - test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh (Vue 3 test app)" |
||||
|
|
||||
@ -0,0 +1,161 @@ |
|||||
|
#!/bin/bash |
||||
|
# Native iOS Development App Build and Deploy Script |
||||
|
# Builds and deploys the ios/App development app to iOS Simulator |
||||
|
# Similar to android/app - a simple Capacitor app for plugin development |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Colors for output |
||||
|
RED='\033[0;31m' |
||||
|
GREEN='\033[0;32m' |
||||
|
YELLOW='\033[1;33m' |
||||
|
BLUE='\033[0;34m' |
||||
|
NC='\033[0m' # No Color |
||||
|
|
||||
|
log_info() { |
||||
|
echo -e "${GREEN}[INFO]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_warn() { |
||||
|
echo -e "${YELLOW}[WARN]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_error() { |
||||
|
echo -e "${RED}[ERROR]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_step() { |
||||
|
echo -e "${BLUE}[STEP]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
# Get script directory |
||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" |
||||
|
|
||||
|
# Check if we're in the project root |
||||
|
if [ ! -d "$PROJECT_ROOT/ios/App" ]; then |
||||
|
log_error "ios/App directory not found" |
||||
|
log_info "This script must be run from the project root directory" |
||||
|
log_info "Usage: cd /path/to/daily-notification-plugin && ./scripts/build-and-deploy-native-ios.sh" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check prerequisites |
||||
|
log_step "Checking prerequisites..." |
||||
|
|
||||
|
if ! command -v xcodebuild &> /dev/null; then |
||||
|
log_error "xcodebuild not found. Install Xcode command line tools:" |
||||
|
log_info " xcode-select --install" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
if ! command -v pod &> /dev/null; then |
||||
|
log_error "CocoaPods not found. Install with:" |
||||
|
log_info " gem install cocoapods" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Get simulator device (default to iPhone 15 Pro) |
||||
|
SIMULATOR_DEVICE="${1:-iPhone 15 Pro}" |
||||
|
log_info "Using simulator: $SIMULATOR_DEVICE" |
||||
|
|
||||
|
# Boot simulator |
||||
|
log_step "Booting simulator..." |
||||
|
if xcrun simctl list devices | grep -q "$SIMULATOR_DEVICE.*Booted"; then |
||||
|
log_info "Simulator already booted" |
||||
|
else |
||||
|
# Try to boot the device |
||||
|
if xcrun simctl boot "$SIMULATOR_DEVICE" 2>/dev/null; then |
||||
|
log_info "✓ Simulator booted" |
||||
|
else |
||||
|
log_warn "Could not boot simulator automatically" |
||||
|
log_info "Opening Simulator app... (you may need to select device manually)" |
||||
|
open -a Simulator |
||||
|
sleep 5 |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Build plugin |
||||
|
log_step "Building plugin..." |
||||
|
cd "$PROJECT_ROOT" |
||||
|
if ! ./scripts/build-native.sh --platform ios; then |
||||
|
log_error "Plugin build failed" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Install CocoaPods dependencies |
||||
|
log_step "Installing CocoaPods dependencies..." |
||||
|
cd "$PROJECT_ROOT/ios" |
||||
|
if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ]; then |
||||
|
pod install |
||||
|
else |
||||
|
log_info "CocoaPods dependencies up to date" |
||||
|
fi |
||||
|
|
||||
|
# Build iOS app |
||||
|
log_step "Building native iOS development app..." |
||||
|
cd "$PROJECT_ROOT/ios/App" |
||||
|
WORKSPACE="App.xcworkspace" |
||||
|
SCHEME="App" |
||||
|
CONFIG="Debug" |
||||
|
SDK="iphonesimulator" |
||||
|
|
||||
|
if ! xcodebuild -workspace "$WORKSPACE" \ |
||||
|
-scheme "$SCHEME" \ |
||||
|
-configuration "$CONFIG" \ |
||||
|
-sdk "$SDK" \ |
||||
|
-destination "platform=iOS Simulator,name=$SIMULATOR_DEVICE" \ |
||||
|
-derivedDataPath build/derivedData \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
clean build; then |
||||
|
log_error "iOS app build failed" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Find built app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
||||
|
|
||||
|
if [ -z "$APP_PATH" ]; then |
||||
|
log_error "Could not find built app" |
||||
|
log_info "Searching in: build/derivedData" |
||||
|
find build/derivedData -name "*.app" -type d 2>/dev/null | head -5 |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
log_info "Found app: $APP_PATH" |
||||
|
|
||||
|
# Install app on simulator |
||||
|
log_step "Installing app on simulator..." |
||||
|
if xcrun simctl install booted "$APP_PATH"; then |
||||
|
log_info "✓ App installed" |
||||
|
else |
||||
|
log_error "Failed to install app" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Get bundle identifier |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist 2>/dev/null || echo "com.timesafari.dailynotification") |
||||
|
log_info "Bundle ID: $BUNDLE_ID" |
||||
|
|
||||
|
# Launch app |
||||
|
log_step "Launching app..." |
||||
|
if xcrun simctl launch booted "$BUNDLE_ID"; then |
||||
|
log_info "✓ App launched" |
||||
|
else |
||||
|
log_warn "App may already be running" |
||||
|
fi |
||||
|
|
||||
|
log_info "" |
||||
|
log_info "✅ Build and deploy complete!" |
||||
|
log_info "" |
||||
|
log_info "To view logs:" |
||||
|
log_info " xcrun simctl spawn booted log stream" |
||||
|
log_info "" |
||||
|
log_info "To uninstall app:" |
||||
|
log_info " xcrun simctl uninstall booted $BUNDLE_ID" |
||||
|
log_info "" |
||||
|
log_info "Note: This is the native iOS development app (ios/App)" |
||||
|
log_info "For the Vue 3 test app, use: test-apps/daily-notification-test/scripts/build-and-deploy-ios.sh" |
||||
|
|
||||
@ -0,0 +1,250 @@ |
|||||
|
#!/bin/bash |
||||
|
# Ruby Version Manager (rbenv) Setup Script |
||||
|
# Installs rbenv and Ruby 3.1+ for CocoaPods compatibility |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Colors for output |
||||
|
RED='\033[0;31m' |
||||
|
GREEN='\033[0;32m' |
||||
|
YELLOW='\033[1;33m' |
||||
|
BLUE='\033[0;34m' |
||||
|
NC='\033[0m' # No Color |
||||
|
|
||||
|
log_info() { |
||||
|
echo -e "${GREEN}[INFO]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_warn() { |
||||
|
echo -e "${YELLOW}[WARN]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_error() { |
||||
|
echo -e "${RED}[ERROR]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_step() { |
||||
|
echo -e "${BLUE}[STEP]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
# Check if rbenv is already installed |
||||
|
if command -v rbenv &> /dev/null; then |
||||
|
log_info "rbenv is already installed" |
||||
|
RBENV_INSTALLED=true |
||||
|
else |
||||
|
log_step "Installing rbenv..." |
||||
|
RBENV_INSTALLED=false |
||||
|
fi |
||||
|
|
||||
|
# Install rbenv via Homebrew (recommended on macOS) |
||||
|
if [ "$RBENV_INSTALLED" = false ]; then |
||||
|
if command -v brew &> /dev/null; then |
||||
|
log_info "Installing rbenv via Homebrew..." |
||||
|
brew install rbenv ruby-build |
||||
|
|
||||
|
# Initialize rbenv in shell |
||||
|
if [ -n "$ZSH_VERSION" ]; then |
||||
|
SHELL_CONFIG="$HOME/.zshrc" |
||||
|
else |
||||
|
SHELL_CONFIG="$HOME/.bash_profile" |
||||
|
fi |
||||
|
|
||||
|
# Add rbenv initialization to shell config |
||||
|
if ! grep -q "rbenv init" "$SHELL_CONFIG" 2>/dev/null; then |
||||
|
log_info "Adding rbenv initialization to $SHELL_CONFIG..." |
||||
|
echo '' >> "$SHELL_CONFIG" |
||||
|
echo '# rbenv initialization' >> "$SHELL_CONFIG" |
||||
|
echo 'eval "$(rbenv init - zsh)"' >> "$SHELL_CONFIG" |
||||
|
fi |
||||
|
|
||||
|
# Load rbenv in current session |
||||
|
eval "$(rbenv init - zsh)" |
||||
|
|
||||
|
log_info "✓ rbenv installed successfully" |
||||
|
else |
||||
|
log_warn "Homebrew not found. Installing rbenv manually..." |
||||
|
|
||||
|
# Manual installation via git |
||||
|
if [ ! -d "$HOME/.rbenv" ]; then |
||||
|
git clone https://github.com/rbenv/rbenv.git ~/.rbenv |
||||
|
fi |
||||
|
|
||||
|
if [ ! -d "$HOME/.rbenv/plugins/ruby-build" ]; then |
||||
|
mkdir -p ~/.rbenv/plugins |
||||
|
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build |
||||
|
fi |
||||
|
|
||||
|
# Add to PATH |
||||
|
export PATH="$HOME/.rbenv/bin:$PATH" |
||||
|
eval "$(rbenv init - zsh)" |
||||
|
|
||||
|
# Add to shell config |
||||
|
if [ -n "$ZSH_VERSION" ]; then |
||||
|
SHELL_CONFIG="$HOME/.zshrc" |
||||
|
else |
||||
|
SHELL_CONFIG="$HOME/.bash_profile" |
||||
|
fi |
||||
|
|
||||
|
if ! grep -q "rbenv init" "$SHELL_CONFIG" 2>/dev/null; then |
||||
|
echo '' >> "$SHELL_CONFIG" |
||||
|
echo '# rbenv initialization' >> "$SHELL_CONFIG" |
||||
|
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> "$SHELL_CONFIG" |
||||
|
echo 'eval "$(rbenv init - zsh)"' >> "$SHELL_CONFIG" |
||||
|
fi |
||||
|
|
||||
|
log_info "✓ rbenv installed manually" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Reload shell config |
||||
|
log_step "Reloading shell configuration..." |
||||
|
if [ -n "$ZSH_VERSION" ]; then |
||||
|
source ~/.zshrc 2>/dev/null || true |
||||
|
else |
||||
|
source ~/.bash_profile 2>/dev/null || true |
||||
|
fi |
||||
|
|
||||
|
# Ensure rbenv is in PATH |
||||
|
export PATH="$HOME/.rbenv/bin:$PATH" |
||||
|
eval "$(rbenv init - zsh)" 2>/dev/null || eval "$(rbenv init - bash)" 2>/dev/null || true |
||||
|
|
||||
|
# Check current Ruby version |
||||
|
log_step "Checking current Ruby version..." |
||||
|
CURRENT_RUBY=$(ruby -v 2>/dev/null | cut -d' ' -f2 | cut -d. -f1,2) || CURRENT_RUBY="unknown" |
||||
|
|
||||
|
if [ "$CURRENT_RUBY" != "unknown" ]; then |
||||
|
RUBY_MAJOR=$(echo "$CURRENT_RUBY" | cut -d. -f1) |
||||
|
RUBY_MINOR=$(echo "$CURRENT_RUBY" | cut -d. -f2) |
||||
|
|
||||
|
if [ "$RUBY_MAJOR" -ge 3 ] && [ "$RUBY_MINOR" -ge 1 ]; then |
||||
|
log_info "✓ Ruby version $CURRENT_RUBY is already >= 3.1.0" |
||||
|
log_info "You can proceed with CocoaPods installation" |
||||
|
exit 0 |
||||
|
else |
||||
|
log_warn "Current Ruby version: $CURRENT_RUBY (needs 3.1.0+)" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Check if rbenv has a suitable Ruby version already installed |
||||
|
log_step "Checking installed Ruby versions..." |
||||
|
if rbenv versions | grep -qE "3\.(1|2|3|4)\."; then |
||||
|
INSTALLED_RUBY=$(rbenv versions | grep -E "3\.(1|2|3|4)\." | tail -1 | sed 's/^[[:space:]]*//' | cut -d' ' -f1) |
||||
|
log_info "Found installed Ruby version: $INSTALLED_RUBY" |
||||
|
|
||||
|
# Set as global if not already set |
||||
|
CURRENT_GLOBAL=$(rbenv global) |
||||
|
if [ "$CURRENT_GLOBAL" != "$INSTALLED_RUBY" ]; then |
||||
|
log_info "Setting $INSTALLED_RUBY as default..." |
||||
|
rbenv global "$INSTALLED_RUBY" |
||||
|
rbenv rehash |
||||
|
fi |
||||
|
|
||||
|
# Verify it works |
||||
|
export PATH="$HOME/.rbenv/bin:$PATH" |
||||
|
eval "$(rbenv init - zsh)" 2>/dev/null || eval "$(rbenv init - bash)" 2>/dev/null || true |
||||
|
|
||||
|
if ruby -rpsych -e "true" 2>/dev/null; then |
||||
|
VERIFIED_RUBY=$(ruby -v) |
||||
|
log_info "✓ Ruby $VERIFIED_RUBY is working correctly" |
||||
|
log_info "" |
||||
|
log_info "Ruby setup complete!" |
||||
|
log_info "" |
||||
|
log_info "Next steps:" |
||||
|
log_info " 1. Reload your shell: source ~/.zshrc" |
||||
|
log_info " 2. Verify Ruby: ruby -v" |
||||
|
log_info " 3. Install CocoaPods: gem install cocoapods" |
||||
|
exit 0 |
||||
|
else |
||||
|
log_warn "Installed Ruby $INSTALLED_RUBY found but psych extension not working" |
||||
|
log_warn "May need to reinstall Ruby or install libyaml dependencies" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Check for libyaml dependency (required for psych extension) |
||||
|
log_step "Checking for libyaml dependency..." |
||||
|
LIBYAML_FOUND=false |
||||
|
if command -v brew &> /dev/null; then |
||||
|
if brew list libyaml &> /dev/null; then |
||||
|
LIBYAML_FOUND=true |
||||
|
log_info "✓ libyaml found via Homebrew" |
||||
|
else |
||||
|
log_warn "libyaml not installed. Installing via Homebrew..." |
||||
|
if brew install libyaml; then |
||||
|
LIBYAML_FOUND=true |
||||
|
log_info "✓ libyaml installed successfully" |
||||
|
else |
||||
|
log_error "Failed to install libyaml via Homebrew" |
||||
|
fi |
||||
|
fi |
||||
|
else |
||||
|
# Check if libyaml headers exist in system locations |
||||
|
if find /usr/local /opt /Library -name "yaml.h" 2>/dev/null | grep -q yaml.h; then |
||||
|
LIBYAML_FOUND=true |
||||
|
log_info "✓ libyaml headers found in system" |
||||
|
else |
||||
|
log_warn "libyaml not found. Ruby installation may fail." |
||||
|
log_warn "Install libyaml via Homebrew: brew install libyaml" |
||||
|
log_warn "Or install via MacPorts: sudo port install libyaml" |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# List available Ruby versions |
||||
|
log_step "Fetching available Ruby versions..." |
||||
|
rbenv install --list | grep -E "^[[:space:]]*3\.[1-9]" | tail -5 || log_warn "Could not fetch Ruby versions list" |
||||
|
|
||||
|
# Install Ruby 3.1.0 (preferred for compatibility) |
||||
|
log_step "Installing Ruby 3.1.0..." |
||||
|
if rbenv install 3.1.0; then |
||||
|
log_info "✓ Ruby 3.1.0 installed successfully" |
||||
|
|
||||
|
# Set as global default |
||||
|
log_step "Setting Ruby 3.1.0 as default..." |
||||
|
rbenv global 3.1.0 |
||||
|
|
||||
|
# Verify installation |
||||
|
export PATH="$HOME/.rbenv/bin:$PATH" |
||||
|
eval "$(rbenv init - zsh)" 2>/dev/null || eval "$(rbenv init - bash)" 2>/dev/null || true |
||||
|
|
||||
|
NEW_RUBY=$(ruby -v) |
||||
|
log_info "✓ Current Ruby version: $NEW_RUBY" |
||||
|
|
||||
|
# Verify psych extension works |
||||
|
if ruby -rpsych -e "true" 2>/dev/null; then |
||||
|
log_info "✓ psych extension verified" |
||||
|
else |
||||
|
log_warn "psych extension may not be working correctly" |
||||
|
log_warn "This may affect CocoaPods installation" |
||||
|
fi |
||||
|
|
||||
|
# Rehash to make Ruby available |
||||
|
rbenv rehash |
||||
|
|
||||
|
log_info "" |
||||
|
log_info "Ruby setup complete!" |
||||
|
log_info "" |
||||
|
log_info "Next steps:" |
||||
|
log_info " 1. Reload your shell: source ~/.zshrc" |
||||
|
log_info " 2. Verify Ruby: ruby -v" |
||||
|
log_info " 3. Install CocoaPods: gem install cocoapods" |
||||
|
|
||||
|
else |
||||
|
log_error "Failed to install Ruby 3.1.0" |
||||
|
|
||||
|
if [ "$LIBYAML_FOUND" = false ]; then |
||||
|
log_error "" |
||||
|
log_error "Installation failed. This is likely due to missing libyaml dependency." |
||||
|
log_error "" |
||||
|
log_error "To fix:" |
||||
|
if command -v brew &> /dev/null; then |
||||
|
log_error " brew install libyaml" |
||||
|
else |
||||
|
log_error " Install Homebrew: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" |
||||
|
log_error " Then: brew install libyaml" |
||||
|
fi |
||||
|
log_error "" |
||||
|
log_error "After installing libyaml, run this script again." |
||||
|
else |
||||
|
log_error "Installation failed. Please check your internet connection and try again." |
||||
|
fi |
||||
|
exit 1 |
||||
|
fi |
||||
@ -0,0 +1,183 @@ |
|||||
|
# iOS Build Process Quick Reference |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Date**: November 4, 2025 |
||||
|
|
||||
|
## Two Different Test Apps |
||||
|
|
||||
|
**Important**: There are two different iOS test apps: |
||||
|
|
||||
|
1. **Native iOS Development App** (`ios/App`) - Simple Capacitor app for quick plugin testing |
||||
|
2. **Vue 3 Test App** (`test-apps/daily-notification-test`) - Full-featured Vue 3 Capacitor app |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Vue 3 Test App (`test-apps/daily-notification-test`) |
||||
|
|
||||
|
### 🚨 Critical Build Steps |
||||
|
|
||||
|
```bash |
||||
|
# 1. Build web assets |
||||
|
npm run build |
||||
|
|
||||
|
# 2. Sync with iOS project |
||||
|
npx cap sync ios |
||||
|
|
||||
|
# 3. Build iOS app |
||||
|
cd ios/App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
|
||||
|
# 4. Install and launch |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
``` |
||||
|
|
||||
|
### 🔄 Using Capacitor CLI (Simplest Method) |
||||
|
|
||||
|
```bash |
||||
|
cd test-apps/daily-notification-test |
||||
|
|
||||
|
# Build and run in one command |
||||
|
npx cap run ios |
||||
|
|
||||
|
# This handles: |
||||
|
# - Building web assets |
||||
|
# - Syncing with iOS |
||||
|
# - Building app |
||||
|
# - Installing on simulator |
||||
|
# - Launching app |
||||
|
``` |
||||
|
|
||||
|
### 🛠️ Automated Build Script |
||||
|
|
||||
|
```bash |
||||
|
cd test-apps/daily-notification-test |
||||
|
./scripts/build-and-deploy-ios.sh |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Native iOS Development App (`ios/App`) |
||||
|
|
||||
|
### 🚨 Critical Build Steps |
||||
|
|
||||
|
```bash |
||||
|
# 1. Build plugin |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
./scripts/build-native.sh --platform ios |
||||
|
|
||||
|
# 2. Install CocoaPods dependencies |
||||
|
cd ios |
||||
|
pod install |
||||
|
|
||||
|
# 3. Build iOS app |
||||
|
cd App |
||||
|
xcodebuild -workspace App.xcworkspace \ |
||||
|
-scheme App \ |
||||
|
-configuration Debug \ |
||||
|
-sdk iphonesimulator \ |
||||
|
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO |
||||
|
|
||||
|
# 4. Install and launch |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d | head -1) |
||||
|
xcrun simctl install booted "$APP_PATH" |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist) |
||||
|
xcrun simctl launch booted "$BUNDLE_ID" |
||||
|
``` |
||||
|
|
||||
|
### 🛠️ Automated Build Script |
||||
|
|
||||
|
```bash |
||||
|
cd /path/to/daily-notification-plugin |
||||
|
./scripts/build-and-deploy-native-ios.sh |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ⚠️ iOS-Specific Requirements |
||||
|
|
||||
|
**Prerequisites:** |
||||
|
- macOS (required for iOS development) |
||||
|
- Xcode installed (`xcode-select --install`) |
||||
|
- CocoaPods installed (`gem install cocoapods`) |
||||
|
- iOS Simulator runtime installed |
||||
|
|
||||
|
**Common Issues:** |
||||
|
- Simulator not booted: `xcrun simctl boot "iPhone 15 Pro"` |
||||
|
- CocoaPods not installed: `sudo gem install cocoapods` |
||||
|
- Platform components missing: `xcodebuild -downloadPlatform iOS` |
||||
|
|
||||
|
## 🔍 Verification Checklist |
||||
|
|
||||
|
After build, verify: |
||||
|
|
||||
|
### For Vue 3 Test App: |
||||
|
- [ ] Simulator is booted (`xcrun simctl list devices | grep Booted`) |
||||
|
- [ ] CocoaPods dependencies installed (`cd ios && pod install`) |
||||
|
- [ ] Web assets synced (`npx cap sync ios`) |
||||
|
- [ ] App builds successfully (`xcodebuild ...`) |
||||
|
- [ ] App installs on simulator (`xcrun simctl install`) |
||||
|
- [ ] App launches (`xcrun simctl launch`) |
||||
|
|
||||
|
### For Native iOS App: |
||||
|
- [ ] Simulator is booted (`xcrun simctl list devices | grep Booted`) |
||||
|
- [ ] Plugin built (`./scripts/build-native.sh --platform ios`) |
||||
|
- [ ] CocoaPods dependencies installed (`cd ios && pod install`) |
||||
|
- [ ] App builds successfully (`xcodebuild ...`) |
||||
|
- [ ] App installs on simulator (`xcrun simctl install`) |
||||
|
- [ ] App launches (`xcrun simctl launch`) |
||||
|
|
||||
|
## 📱 Testing Commands |
||||
|
|
||||
|
```bash |
||||
|
# List available simulators |
||||
|
xcrun simctl list devices available |
||||
|
|
||||
|
# Boot simulator |
||||
|
xcrun simctl boot "iPhone 15 Pro" |
||||
|
|
||||
|
# Check if booted |
||||
|
xcrun simctl list devices | grep Booted |
||||
|
|
||||
|
# View logs |
||||
|
xcrun simctl spawn booted log stream |
||||
|
|
||||
|
# Uninstall app |
||||
|
xcrun simctl uninstall booted com.timesafari.dailynotification.test # Vue 3 app |
||||
|
xcrun simctl uninstall booted com.timesafari.dailynotification # Native app |
||||
|
|
||||
|
# Reset simulator |
||||
|
xcrun simctl erase booted |
||||
|
``` |
||||
|
|
||||
|
## 🐛 Common Issues |
||||
|
|
||||
|
| Issue | Symptom | Solution | |
||||
|
|-------|---------|----------| |
||||
|
| Simulator not found | `Unable to find destination` | Run `xcrun simctl list devices` to see available devices | |
||||
|
| CocoaPods error | `pod: command not found` | Install CocoaPods: `gem install cocoapods` | |
||||
|
| Build fails | `No such file or directory` | Run `pod install` in `ios/` directory | |
||||
|
| Signing error | `Code signing required` | Add `CODE_SIGNING_REQUIRED=NO` to xcodebuild command | |
||||
|
| App won't install | `Could not find application` | Verify app path exists and simulator is booted | |
||||
|
| Vue app: assets not syncing | App shows blank screen | Run `npm run build && npx cap sync ios` | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Remember**: |
||||
|
- **Native iOS App** (`ios/App`) = Quick plugin testing, no web build needed |
||||
|
- **Vue 3 Test App** (`test-apps/...`) = Full testing with UI, requires `npm run build` |
||||
|
|
||||
|
Use `npx cap run ios` in the Vue 3 test app directory for the simplest workflow! |
||||
|
|
||||
@ -0,0 +1,150 @@ |
|||||
|
#!/bin/bash |
||||
|
# iOS Test App Build and Deploy Script |
||||
|
# Builds and deploys the DailyNotification test app to iOS Simulator |
||||
|
|
||||
|
set -e |
||||
|
|
||||
|
# Colors for output |
||||
|
RED='\033[0;31m' |
||||
|
GREEN='\033[0;32m' |
||||
|
YELLOW='\033[1;33m' |
||||
|
BLUE='\033[0;34m' |
||||
|
NC='\033[0m' # No Color |
||||
|
|
||||
|
log_info() { |
||||
|
echo -e "${GREEN}[INFO]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_warn() { |
||||
|
echo -e "${YELLOW}[WARN]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_error() { |
||||
|
echo -e "${RED}[ERROR]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
log_step() { |
||||
|
echo -e "${BLUE}[STEP]${NC} $1" |
||||
|
} |
||||
|
|
||||
|
# Check if we're in the test app directory |
||||
|
if [ ! -f "package.json" ] || [ ! -d "ios" ]; then |
||||
|
log_error "This script must be run from test-apps/daily-notification-test directory" |
||||
|
log_info "Usage: cd test-apps/daily-notification-test && ./scripts/build-and-deploy-ios.sh" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Check prerequisites |
||||
|
log_step "Checking prerequisites..." |
||||
|
|
||||
|
if ! command -v xcodebuild &> /dev/null; then |
||||
|
log_error "xcodebuild not found. Install Xcode command line tools:" |
||||
|
log_info " xcode-select --install" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
if ! command -v pod &> /dev/null; then |
||||
|
log_error "CocoaPods not found. Install with:" |
||||
|
log_info " gem install cocoapods" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Get simulator device (default to iPhone 15 Pro) |
||||
|
SIMULATOR_DEVICE="${1:-iPhone 15 Pro}" |
||||
|
log_info "Using simulator: $SIMULATOR_DEVICE" |
||||
|
|
||||
|
# Boot simulator |
||||
|
log_step "Booting simulator..." |
||||
|
if xcrun simctl list devices | grep -q "$SIMULATOR_DEVICE.*Booted"; then |
||||
|
log_info "Simulator already booted" |
||||
|
else |
||||
|
# Try to boot the device |
||||
|
if xcrun simctl boot "$SIMULATOR_DEVICE" 2>/dev/null; then |
||||
|
log_info "✓ Simulator booted" |
||||
|
else |
||||
|
log_warn "Could not boot simulator automatically" |
||||
|
log_info "Opening Simulator app... (you may need to select device manually)" |
||||
|
open -a Simulator |
||||
|
sleep 5 |
||||
|
fi |
||||
|
fi |
||||
|
|
||||
|
# Build web assets |
||||
|
log_step "Building web assets..." |
||||
|
npm run build |
||||
|
|
||||
|
# Sync with iOS |
||||
|
log_step "Syncing with iOS project..." |
||||
|
if ! npx cap sync ios; then |
||||
|
log_error "Failed to sync with iOS project" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Install CocoaPods dependencies |
||||
|
log_step "Installing CocoaPods dependencies..." |
||||
|
cd ios/App |
||||
|
if [ ! -f "Podfile.lock" ] || [ "Podfile" -nt "Podfile.lock" ]; then |
||||
|
pod install |
||||
|
else |
||||
|
log_info "CocoaPods dependencies up to date" |
||||
|
fi |
||||
|
|
||||
|
# Build iOS app |
||||
|
log_step "Building iOS app..." |
||||
|
WORKSPACE="App.xcworkspace" |
||||
|
SCHEME="App" |
||||
|
CONFIG="Debug" |
||||
|
SDK="iphonesimulator" |
||||
|
|
||||
|
xcodebuild -workspace "$WORKSPACE" \ |
||||
|
-scheme "$SCHEME" \ |
||||
|
-configuration "$CONFIG" \ |
||||
|
-sdk "$SDK" \ |
||||
|
-destination "platform=iOS Simulator,name=$SIMULATOR_DEVICE" \ |
||||
|
-derivedDataPath build/derivedData \ |
||||
|
CODE_SIGN_IDENTITY="" \ |
||||
|
CODE_SIGNING_REQUIRED=NO \ |
||||
|
CODE_SIGNING_ALLOWED=NO \ |
||||
|
clean build |
||||
|
|
||||
|
# Find built app |
||||
|
APP_PATH=$(find build/derivedData -name "*.app" -type d -path "*/Build/Products/*-iphonesimulator/*.app" | head -1) |
||||
|
|
||||
|
if [ -z "$APP_PATH" ]; then |
||||
|
log_error "Could not find built app" |
||||
|
log_info "Searching in: build/derivedData" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
log_info "Found app: $APP_PATH" |
||||
|
|
||||
|
# Install app on simulator |
||||
|
log_step "Installing app on simulator..." |
||||
|
if xcrun simctl install booted "$APP_PATH"; then |
||||
|
log_info "✓ App installed" |
||||
|
else |
||||
|
log_error "Failed to install app" |
||||
|
exit 1 |
||||
|
fi |
||||
|
|
||||
|
# Get bundle identifier |
||||
|
BUNDLE_ID=$(plutil -extract CFBundleIdentifier raw App/Info.plist 2>/dev/null || echo "com.timesafari.dailynotification.test") |
||||
|
log_info "Bundle ID: $BUNDLE_ID" |
||||
|
|
||||
|
# Launch app |
||||
|
log_step "Launching app..." |
||||
|
if xcrun simctl launch booted "$BUNDLE_ID"; then |
||||
|
log_info "✓ App launched" |
||||
|
else |
||||
|
log_warn "App may already be running" |
||||
|
fi |
||||
|
|
||||
|
log_info "" |
||||
|
log_info "✅ Build and deploy complete!" |
||||
|
log_info "" |
||||
|
log_info "To view logs:" |
||||
|
log_info " xcrun simctl spawn booted log stream" |
||||
|
log_info "" |
||||
|
log_info "To uninstall app:" |
||||
|
log_info " xcrun simctl uninstall booted $BUNDLE_ID" |
||||
|
|
||||
Loading…
Reference in new issue