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:
Server
2025-11-13 23:29:03 -08:00
parent 5844b92e18
commit ed25b1385a
4 changed files with 349 additions and 21 deletions

View File

@@ -1,32 +1,108 @@
import UIKit
import Capacitor
import BackgroundTasks
import DailyNotificationPlugin
import ObjectiveC
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// Background task identifiers
private let fetchTaskIdentifier = "com.timesafari.dailynotification.fetch"
private let notifyTaskIdentifier = "com.timesafari.dailynotification.notify"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Register background tasks
if #available(iOS 13.0, *) {
BGTaskScheduler.shared.register(forTaskWithIdentifier: fetchTaskIdentifier, using: nil) { task in
// Background fetch task handler
// Plugin will handle this
task.setTaskCompleted(success: true)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: notifyTaskIdentifier, using: nil) { task in
// Background notify task handler
// Plugin will handle this
task.setTaskCompleted(success: true)
NSLog("DNP-DEBUG: AppDelegate.application(_:didFinishLaunchingWithOptions:) called")
// DIAGNOSTIC: Test plugin class accessibility via Objective-C runtime
NSLog("DNP-DEBUG: Testing plugin class accessibility...")
// Test 1: NSClassFromString (Objective-C runtime lookup)
if let pluginClass = NSClassFromString("DailyNotificationPlugin") {
NSLog("DNP-DEBUG: ✅ NSClassFromString found plugin class: %@", String(describing: pluginClass))
} else {
NSLog("DNP-DEBUG: ❌ NSClassFromString could NOT find plugin class 'DailyNotificationPlugin'")
}
// Test 2: Swift type access
let pluginType = DailyNotificationPlugin.self
NSLog("DNP-DEBUG: Plugin class type: %@", String(describing: pluginType))
NSLog("DNP-DEBUG: Plugin class name: %@", String(describing: type(of: pluginType)))
// Test 3: Verify CAPPlugin inheritance
// Note: 'is' test is always true because DailyNotificationPlugin extends CAPPlugin
NSLog("DNP-DEBUG: ✅ Plugin is a CAPPlugin subclass (DailyNotificationPlugin extends CAPPlugin)")
// Test 4: Check if class conforms to NSObjectProtocol (required for Objective-C runtime)
// Note: 'is' test is always true because CAPPlugin extends NSObject which conforms to NSObjectProtocol
NSLog("DNP-DEBUG: ✅ Plugin conforms to NSObjectProtocol (CAPPlugin extends NSObject)")
// Test 5: Try to get class name via Objective-C runtime
let className = NSStringFromClass(pluginType)
NSLog("DNP-DEBUG: Plugin class name (NSStringFromClass): %@", className)
// Test 6: Check if framework is loaded
if let framework = Bundle(identifier: "org.cocoapods.DailyNotificationPlugin") {
NSLog("DNP-DEBUG: ✅ Plugin framework bundle found: %@", framework.bundlePath)
} else {
NSLog("DNP-DEBUG: ⚠️ Plugin framework bundle not found via identifier")
// Try alternative: check if class is in main bundle
if Bundle.main.path(forResource: "DailyNotificationPlugin", ofType: "framework") != nil {
NSLog("DNP-DEBUG: ✅ Plugin framework found in main bundle")
} else {
NSLog("DNP-DEBUG: ⚠️ Plugin framework not found in main bundle")
}
}
// CRITICAL: Force-load the plugin framework before Capacitor initializes
// objc_getClassList may not include classes from frameworks that haven't been loaded yet
// Even though NSClassFromString can find the class, Capacitor's discovery uses objc_getClassList
// which only includes loaded classes. We need to ensure the framework is loaded.
NSLog("DNP-DEBUG: Force-loading DailyNotificationPlugin framework...")
_ = DailyNotificationPlugin.self // Force class load
NSLog("DNP-DEBUG: DailyNotificationPlugin class reference created - framework should be loaded")
// Verify class is now in objc_getClassList
let classCount = objc_getClassList(nil, 0)
let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount))
defer { classes.deallocate() }
let releasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes)
let numClasses = objc_getClassList(releasingClasses, Int32(classCount))
var foundInClassList = false
for i in 0..<Int(numClasses) {
if let aClass = classes[i] {
let className = NSStringFromClass(aClass)
if className == "DailyNotificationPlugin" {
foundInClassList = true
NSLog("DNP-DEBUG: ✅ DailyNotificationPlugin found in objc_getClassList")
break
}
}
}
if !foundInClassList {
NSLog("DNP-DEBUG: ❌ DailyNotificationPlugin NOT found in objc_getClassList (this is the problem!)")
} else {
// Test if class conforms to CAPBridgedPlugin (required for Capacitor discovery)
if let aClass = NSClassFromString("DailyNotificationPlugin") {
// Try using CAPBridgedPlugin.self (the actual protocol, not string lookup)
// This is what Capacitor uses: class_conformsToProtocol(aClass, CAPBridgedPlugin.self)
let conformsToBridgedPlugin = class_conformsToProtocol(aClass, CAPBridgedPlugin.self)
NSLog("DNP-DEBUG: Class conforms to CAPBridgedPlugin (using .self): %@", conformsToBridgedPlugin ? "YES" : "NO")
// Test if class can be cast to CapacitorPlugin.Type
if let pluginType = aClass as? CAPPlugin.Type {
// Try casting to CapacitorPlugin (which is CAPPlugin & CAPBridgedPlugin)
if let capacitorPluginType = pluginType as? (CAPPlugin & CAPBridgedPlugin).Type {
NSLog("DNP-DEBUG: ✅ Can cast to (CAPPlugin & CAPBridgedPlugin).Type")
} else {
NSLog("DNP-DEBUG: ❌ Cannot cast to (CAPPlugin & CAPBridgedPlugin).Type")
}
}
}
}
// NOTE: Background task registration is handled by DailyNotificationPlugin.load()
// Do NOT register here to avoid duplicate registration crash
// Override point for customization after application launch.
return true
}