fix(ios): enable Capacitor plugin discovery via CAPBridgedPlugin conformance
Capacitor iOS was not discovering DailyNotificationPlugin because it did not conform to the CAPBridgedPlugin protocol required for runtime discovery. Changes: - Add @objc extension to DailyNotificationPlugin implementing CAPBridgedPlugin with identifier, jsName, and pluginMethods properties - Force-load plugin framework in AppDelegate before Capacitor initializes - Remove duplicate BGTaskScheduler registration from AppDelegate (plugin handles it) - Update podspec to use dynamic framework (static_framework = false) - Add diagnostic logging to verify plugin discovery Result: Plugin is now discovered by Capacitor and all methods are accessible from JavaScript. Verified working with checkPermissionStatus() method. Files modified: - ios/Plugin/DailyNotificationPlugin.swift: Added CAPBridgedPlugin extension - test-apps/ios-test-app/ios/App/App/AppDelegate.swift: Force-load + diagnostics - ios/DailyNotificationPlugin.podspec: Dynamic framework setting - doc/directives/0003-iOS-Android-Parity-Directive.md: Documented solution
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# iOS Android Parity Directive — iOS Implementation Upgrade
|
||||
|
||||
**Status:** 🎯 **PLANNING** - Directive for iOS-2 branch
|
||||
**Status:** ✅ **IN PROGRESS** - Plugin discovery resolved, implementation continuing
|
||||
**Date:** 2025-11-13
|
||||
**Author:** Matthew Raymer
|
||||
**Branch:** `ios-2`
|
||||
@@ -36,6 +36,213 @@ This directive outlines the plan to upgrade the iOS implementation of the Daily
|
||||
|
||||
---
|
||||
|
||||
## Plugin Discovery Issue - Systematic Analysis
|
||||
|
||||
**Date:** 2025-11-13
|
||||
**Status:** ✅ **RESOLVED**
|
||||
|
||||
### Problem Statement
|
||||
|
||||
Capacitor iOS is not discovering the `DailyNotificationPlugin` class, preventing plugin methods from being accessible from JavaScript.
|
||||
|
||||
**Symptoms:**
|
||||
- JavaScript reports: `DailyNotification plugin NOT found`
|
||||
- Available plugins only show: `["WebView","Console","CapacitorHttp","CapacitorCookies"]`
|
||||
- Plugin's `load()` method is never called
|
||||
- Plugin class exists and is properly linked (verified in build settings)
|
||||
|
||||
### Current Configuration
|
||||
|
||||
**Verified Working:**
|
||||
- ✅ Plugin class correctly annotated: `@objc(DailyNotificationPlugin)`
|
||||
- ✅ Plugin inherits from `CAPPlugin`
|
||||
- ✅ Framework is linked: `-framework "DailyNotificationPlugin"` in build settings
|
||||
- ✅ `capacitor.plugins.json` exists with entry: `{"name":"DailyNotification","class":"DailyNotificationPlugin"}`
|
||||
- ✅ Podfile references plugin: `pod 'DailyNotificationPlugin', :path => '../../../../ios'`
|
||||
- ✅ Podspec configured: `static_framework = true`
|
||||
|
||||
**Potential Issues to Investigate:**
|
||||
|
||||
1. **Static Framework Discovery:**
|
||||
- Capacitor iOS may not scan static frameworks for plugins
|
||||
- Static frameworks are linked but may not be in scanned bundle
|
||||
- **Test:** Remove `static_framework = true`, run `pod install`, rebuild
|
||||
|
||||
2. **Framework Loading:**
|
||||
- Framework may not be loaded into Objective-C runtime
|
||||
- Classes in static frameworks may not be discoverable via `objc_getClassList`
|
||||
- **Test:** Add explicit framework load in AppDelegate
|
||||
|
||||
3. **Module/Namespace Issues:**
|
||||
- Plugin may be in separate module that Capacitor doesn't scan
|
||||
- `use_frameworks!` creates separate framework modules
|
||||
- **Test:** Check if plugin class is accessible via `NSClassFromString`
|
||||
|
||||
4. **Capacitor Discovery Mechanism:**
|
||||
- iOS may use different discovery than Android
|
||||
- May require explicit registration vs. automatic discovery
|
||||
- **Test:** Check Capacitor source code for discovery mechanism
|
||||
|
||||
5. **Timing Issues:**
|
||||
- Plugin discovery may happen before framework is loaded
|
||||
- AppDelegate may need to force-load framework before Capacitor initializes
|
||||
- **Test:** Add framework load in `application(_:didFinishLaunchingWithOptions:)`
|
||||
|
||||
### Investigation Plan
|
||||
|
||||
**Step 1: Verify Plugin Class Accessibility** ✅ **IN PROGRESS**
|
||||
- ✅ Added diagnostic logging to check if `NSClassFromString("DailyNotificationPlugin")` can find the class
|
||||
- ✅ Added logging to verify CAPPlugin inheritance
|
||||
- ✅ Added logging to check NSObjectProtocol conformance
|
||||
- ✅ Added logging to check framework bundle loading
|
||||
- ✅ Added logging in plugin's `load()` method to verify if Capacitor calls it
|
||||
- **Next:** Rebuild app and check Xcode console for diagnostic output
|
||||
|
||||
**Step 2: Test Static Framework Hypothesis**
|
||||
- Temporarily remove `static_framework = true` from podspec
|
||||
- Run `pod install` to regenerate Pods
|
||||
- Rebuild and test if plugin is discovered
|
||||
- If discovered, static framework is the issue
|
||||
|
||||
**Step 3: Test Framework Loading**
|
||||
- Add explicit framework load in AppDelegate before Capacitor initializes
|
||||
- Use `Bundle.load()` or similar to force-load framework
|
||||
- Check if this enables discovery
|
||||
|
||||
**Step 4: Check Capacitor Discovery Code**
|
||||
- Review Capacitor iOS source for plugin discovery mechanism
|
||||
- Verify if it scans static frameworks or only dynamic frameworks
|
||||
- Check if there's a registration API we should use
|
||||
|
||||
**Step 5: Alternative Solutions**
|
||||
- If static framework is the issue, consider:
|
||||
- Removing `static_framework = true` (makes it dynamic)
|
||||
- Adding plugin files directly to app target (bypasses CocoaPods framework)
|
||||
- Using explicit plugin registration API (if available)
|
||||
|
||||
### Diagnostic Logging Added (2025-11-13)
|
||||
|
||||
**Files Modified:**
|
||||
- `test-apps/ios-test-app/ios/App/App/AppDelegate.swift` - Added comprehensive diagnostic tests
|
||||
- `ios/Plugin/DailyNotificationPlugin.swift` - Added logging in `load()` method
|
||||
|
||||
**Diagnostic Tests:**
|
||||
1. **NSClassFromString Test:** Checks if Objective-C runtime can find the plugin class
|
||||
2. **Swift Type Access:** Verifies plugin class is accessible via Swift
|
||||
3. **CAPPlugin Inheritance:** Confirms plugin is a CAPPlugin subclass
|
||||
4. **NSObjectProtocol Conformance:** Verifies Objective-C runtime compatibility
|
||||
5. **Class Name Retrieval:** Gets class name via `NSStringFromClass()`
|
||||
6. **Framework Bundle Check:** Verifies if framework bundle is loaded
|
||||
7. **Plugin Load Method:** Logs when Capacitor calls `load()` (indicates discovery)
|
||||
|
||||
**Expected Output:**
|
||||
When app launches, Xcode console should show:
|
||||
- `DNP-DEBUG: AppDelegate.application(_:didFinishLaunchingWithOptions:) called`
|
||||
- `DNP-DEBUG: Testing plugin class accessibility...`
|
||||
- Either `✅ NSClassFromString found plugin class` or `❌ NSClassFromString could NOT find plugin class`
|
||||
- Either `✅ Plugin is a CAPPlugin subclass` or `❌ Plugin is NOT a CAPPlugin subclass`
|
||||
- `DNP-DEBUG: Plugin class name (NSStringFromClass): DailyNotificationPlugin`
|
||||
- Framework bundle status
|
||||
- **If plugin is discovered:** `DNP-DEBUG: DailyNotificationPlugin.load() called - Capacitor discovered the plugin!`
|
||||
- **If plugin is NOT discovered:** No `load()` log message
|
||||
|
||||
### Diagnostic Results (2025-11-13)
|
||||
|
||||
**Test Results:**
|
||||
- ✅ **NSClassFromString found plugin class:** `DailyNotificationPlugin` - Class IS accessible to Objective-C runtime
|
||||
- ✅ **Plugin is a CAPPlugin subclass:** Inheritance confirmed
|
||||
- ✅ **Plugin conforms to NSObjectProtocol:** Objective-C runtime compatibility verified
|
||||
- ✅ **Plugin class name (NSStringFromClass):** `DailyNotificationPlugin` - Class name correct
|
||||
- ⚠️ **Plugin framework bundle not found via identifier:** Expected for static frameworks (not a separate bundle)
|
||||
- ⚠️ **Plugin framework not found in main bundle:** Expected for CocoaPods frameworks
|
||||
- ❌ **NO `load()` method called:** Capacitor is NOT discovering the plugin
|
||||
|
||||
**Critical Finding (Initial):**
|
||||
The plugin class **IS accessible** to the Objective-C runtime (`NSClassFromString` works), but Capacitor's `load()` method is **NEVER called**. This confirmed:
|
||||
1. **Plugin is properly linked** - Class exists and is accessible
|
||||
2. **Plugin is correctly configured** - Inheritance and runtime compatibility verified
|
||||
3. **Capacitor discovery is failing** - Plugin is not being discovered despite being accessible
|
||||
|
||||
**Root Cause Discovered:**
|
||||
The plugin class did NOT conform to `CAPBridgedPlugin` protocol, which is required for Capacitor's discovery mechanism. Even though the class was in `objc_getClassList()`, `class_conformsToProtocol(aClass, CAPBridgedPlugin.self)` returned `NO`.
|
||||
|
||||
**Solution Implemented (2025-11-13):**
|
||||
|
||||
1. **Added `CAPBridgedPlugin` conformance** via `@objc` extension:
|
||||
- Implemented `identifier` property (returns `"com.timesafari.dailynotification"`)
|
||||
- Implemented `jsName` property (returns `"DailyNotification"`)
|
||||
- Implemented `pluginMethods` property (returns array of all `@objc` methods)
|
||||
|
||||
2. **Force-load framework** in AppDelegate before Capacitor initializes:
|
||||
- Added `_ = DailyNotificationPlugin.self` to ensure class is in `objc_getClassList()`
|
||||
|
||||
3. **Removed duplicate BGTaskScheduler registration** from AppDelegate (plugin handles it)
|
||||
|
||||
**Files Modified:**
|
||||
- `ios/Plugin/DailyNotificationPlugin.swift` - Added `@objc extension DailyNotificationPlugin: CAPBridgedPlugin`
|
||||
- `test-apps/ios-test-app/ios/App/App/AppDelegate.swift` - Added force-load, removed duplicate registration
|
||||
|
||||
### Final Test Results (2025-11-13)
|
||||
|
||||
**Status:** ✅ **SUCCESS** - Plugin is now discovered and working
|
||||
|
||||
**Verification Logs:**
|
||||
- ✅ `Class conforms to CAPBridgedPlugin (using .self): YES`
|
||||
- ✅ `DailyNotificationPlugin.load() called - Capacitor discovered the plugin!`
|
||||
- ✅ `DailyNotification plugin found` (JavaScript)
|
||||
- ✅ All methods accessible: `["addListener","configure","scheduleDailyNotification","getLastNotification","cancelAllNotifications","getNotificationStatus","updateSettings","checkPermissionStatus","requestNotificationPermissions",...]`
|
||||
|
||||
**Why This Works:**
|
||||
- Built-in Capacitor plugins use the `CAP_PLUGIN` macro in Objective-C to add `CAPBridgedPlugin` conformance
|
||||
- Swift plugins must implement it manually via an `@objc` extension
|
||||
- The extension makes the protocol conformance visible to Objective-C runtime's `class_conformsToProtocol()`
|
||||
- Force-loading ensures the class is in `objc_getClassList()` when Capacitor scans
|
||||
|
||||
### Root Cause Discovery (2025-11-13)
|
||||
|
||||
**Critical Finding:** Capacitor iOS uses `objc_getClassList()` to discover plugins, not `NSClassFromString()`.
|
||||
|
||||
**How Capacitor Discovers Plugins:**
|
||||
1. Calls `objc_getClassList()` to get all loaded classes
|
||||
2. Checks if class conforms to `CAPBridgedPlugin` protocol
|
||||
3. Checks if class is a `CapacitorPlugin` type (CAPPlugin & CAPBridgedPlugin)
|
||||
4. Calls `registerPlugin()` for each matching class
|
||||
|
||||
**The Problem:**
|
||||
- `objc_getClassList()` only includes classes from **loaded** frameworks
|
||||
- Even though `NSClassFromString("DailyNotificationPlugin")` works (class is accessible), the class may not be in `objc_getClassList()` if the framework isn't loaded yet
|
||||
- **More critically:** The plugin class did NOT conform to `CAPBridgedPlugin` protocol, which is required for discovery
|
||||
|
||||
**Solution Implemented:**
|
||||
|
||||
1. **Force-load framework** in AppDelegate before Capacitor initializes:
|
||||
- Added `_ = DailyNotificationPlugin.self` in AppDelegate to force class load
|
||||
- Added diagnostic check to verify class is in `objc_getClassList()`
|
||||
|
||||
2. **Add CAPBridgedPlugin conformance** via `@objc` extension:
|
||||
- Implemented `identifier` property (returns `"com.timesafari.dailynotification"`)
|
||||
- Implemented `jsName` property (returns `"DailyNotification"`)
|
||||
- Implemented `pluginMethods` property (returns array of all `@objc` methods)
|
||||
|
||||
**Why This Works:**
|
||||
- Built-in Capacitor plugins use the `CAP_PLUGIN` macro in Objective-C to add `CAPBridgedPlugin` conformance
|
||||
- Swift plugins must implement it manually via an `@objc` extension
|
||||
- The extension makes the protocol conformance visible to Objective-C runtime's `class_conformsToProtocol()`
|
||||
|
||||
**Result:**
|
||||
- ✅ Plugin class is in `objc_getClassList()`
|
||||
- ✅ Plugin conforms to `CAPBridgedPlugin` protocol
|
||||
- ✅ Plugin is discovered by Capacitor
|
||||
- ✅ `DailyNotificationPlugin.load()` is called
|
||||
- ✅ Plugin appears in `window.Capacitor.Plugins.DailyNotification`
|
||||
- ✅ All methods are accessible from JavaScript
|
||||
|
||||
**Files Modified:**
|
||||
- `ios/Plugin/DailyNotificationPlugin.swift` - Added `@objc extension DailyNotificationPlugin: CAPBridgedPlugin` with required properties
|
||||
- `test-apps/ios-test-app/ios/App/App/AppDelegate.swift` - Added force-load and diagnostic checks, removed duplicate BGTaskScheduler registration
|
||||
|
||||
---
|
||||
|
||||
## Minimal Viable Parity Definition
|
||||
|
||||
**Objective:** Define the exact minimal scope iOS must meet before Android parity is considered achieved for Phase 1.
|
||||
@@ -1183,8 +1390,6 @@ scripts/
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Validation Matrix
|
||||
|
||||
**Cross-platform feature validation checklist**
|
||||
@@ -1308,4 +1513,3 @@ scripts/
|
||||
- Ready for functional testing
|
||||
|
||||
**Lessons Learned:** See Decision Log section above for compilation error fixes and patterns.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user