diff --git a/.gitmodules b/.gitmodules index 99f56e3f..b15c8b67 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "ios/Classes/countly-sdk-ios"] - path = ios/Classes/countly-sdk-ios +[submodule "ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios"] + path = ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios url = https://github.com/Countly/countly-sdk-ios.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 0693e008..5a32984d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## XX.XX.XX +* ! Minor breaking change ! The iOS plugin sources were reorganized to support Swift Package Manager. If you use a Notification Service Extension for rich push notifications, update the reference to "CountlyNotificationService.h" and "CountlyNotificationService.m" in your extension target to the new path "ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios/". + +* Added Swift Package Manager (SwiftPM) support for iOS. CocoaPods integration is still supported. + * Mitigated an issue where async native callbacks could crash with a NullPointerException when invoked after the Flutter engine had detached (e.g. hot restart, multi-engine setups) in Android. ## 26.1.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 0825d741..85787c86 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -58,8 +58,8 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2F5416FE25A893D50047E5F9 /* CountlyNotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = CountlyNotificationService.m; path = "../../ios/Classes/countly-sdk-ios/CountlyNotificationService.m"; sourceTree = ""; }; - 2F54170025A893D70047E5F9 /* CountlyNotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CountlyNotificationService.h; path = ".symlinks/plugins/countly_flutter/ios/Classes/countly-sdk-ios/../../../../../../../../ios/Classes/countly-sdk-ios/CountlyNotificationService.h"; sourceTree = ""; }; + 2F5416FE25A893D50047E5F9 /* CountlyNotificationService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = CountlyNotificationService.m; path = "../../ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios/CountlyNotificationService.m"; sourceTree = ""; }; + 2F54170025A893D70047E5F9 /* CountlyNotificationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CountlyNotificationService.h; path = "../../ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios/CountlyNotificationService.h"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 43A5439BF4A8475C0F1E7A45 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; diff --git a/ios/.gitignore b/ios/.gitignore index aa479fd3..0ea997fd 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -34,4 +34,8 @@ Icon? .tags* /Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file +/Flutter/flutter_export_environment.sh + +# Swift Package Manager +.build/ +.swiftpm/ \ No newline at end of file diff --git a/ios/countly_flutter.podspec b/ios/countly_flutter.podspec index fba6248e..4f8ae80f 100644 --- a/ios/countly_flutter.podspec +++ b/ios/countly_flutter.podspec @@ -9,8 +9,10 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/gocountly' s.author = {'Countly' => 'hello@count.ly'} s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/CountlyFlutterPlugin.h' + # Sources live in the Swift Package Manager layout; CocoaPods and SwiftPM share the same files. + s.source_files = 'countly_flutter/Sources/countly_flutter/**/*.{h,m,swift}' + s.public_header_files = 'countly_flutter/Sources/countly_flutter/include/countly_flutter/CountlyFlutterPlugin.h' + s.resource_bundles = { 'countly_flutter_privacy' => ['countly_flutter/Sources/countly_flutter/countly-sdk-ios/PrivacyInfo.xcprivacy'] } s.dependency 'Flutter' s.swift_version = '5.0' s.ios.deployment_target = '10.0' diff --git a/ios/countly_flutter/Package.swift b/ios/countly_flutter/Package.swift new file mode 100644 index 00000000..84051202 --- /dev/null +++ b/ios/countly_flutter/Package.swift @@ -0,0 +1,58 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "countly_flutter", + platforms: [ + // Matches the vendored Countly iOS SDK (its Package.swift and podspecs all target iOS 10), + // and this plugin's own CocoaPods podspec (10.0). A package floor at or below the consumer's + // deployment target is required — SwiftPM rejects a package minimum higher than the app target. + .iOS(.v10) + ], + products: [ + // The library name replaces "_" with "-" per SwiftPM convention. + .library(name: "countly-flutter", targets: ["countly_flutter"]) + ], + dependencies: [], + targets: [ + .target( + name: "countly_flutter", + dependencies: [], + // The Countly iOS SDK is vendored as a git submodule under countly-sdk-ios/. + // Exclude its non-source files, plus the unused default Swift plugin stub + // (SwiftPM does not allow Swift and Objective-C in the same target). + exclude: [ + "SwiftCountlyFlutterPlugin.swift", + "countly-sdk-ios/CHANGELOG.md", + "countly-sdk-ios/README.md", + "countly-sdk-ios/SECURITY.md", + "countly-sdk-ios/LICENSE", + "countly-sdk-ios/Countly.podspec", + "countly-sdk-ios/Countly-PL.podspec", + "countly-sdk-ios/countly_dsym_uploader.sh", + "countly-sdk-ios/format.sh" + ], + resources: [ + .process("countly-sdk-ios/PrivacyInfo.xcprivacy") + ], + cSettings: [ + // Resolve the flat #import "..." statements used by the bridge and the + // vendored Countly iOS SDK without editing every source file. + .headerSearchPath("include/countly_flutter"), + .headerSearchPath("countly-sdk-ios"), + .headerSearchPath(".") + ], + linkerSettings: [ + .linkedFramework("Foundation"), + .linkedFramework("UIKit"), + .linkedFramework("UserNotifications"), + .linkedFramework("CoreLocation"), + .linkedFramework("WebKit"), + .linkedFramework("CoreTelephony"), + .linkedFramework("WatchConnectivity") + ] + ) + ] +) diff --git a/ios/Classes/CountlyFLPushNotifications.h b/ios/countly_flutter/Sources/countly_flutter/CountlyFLPushNotifications.h similarity index 100% rename from ios/Classes/CountlyFLPushNotifications.h rename to ios/countly_flutter/Sources/countly_flutter/CountlyFLPushNotifications.h diff --git a/ios/Classes/CountlyFLPushNotifications.m b/ios/countly_flutter/Sources/countly_flutter/CountlyFLPushNotifications.m similarity index 100% rename from ios/Classes/CountlyFLPushNotifications.m rename to ios/countly_flutter/Sources/countly_flutter/CountlyFLPushNotifications.m diff --git a/ios/Classes/CountlyFlutterPlugin.m b/ios/countly_flutter/Sources/countly_flutter/CountlyFlutterPlugin.m similarity index 100% rename from ios/Classes/CountlyFlutterPlugin.m rename to ios/countly_flutter/Sources/countly_flutter/CountlyFlutterPlugin.m diff --git a/ios/Classes/SwiftCountlyFlutterPlugin.swift b/ios/countly_flutter/Sources/countly_flutter/SwiftCountlyFlutterPlugin.swift similarity index 100% rename from ios/Classes/SwiftCountlyFlutterPlugin.swift rename to ios/countly_flutter/Sources/countly_flutter/SwiftCountlyFlutterPlugin.swift diff --git a/ios/Classes/countly-sdk-ios b/ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios similarity index 100% rename from ios/Classes/countly-sdk-ios rename to ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios diff --git a/ios/Classes/CountlyFlutterPlugin.h b/ios/countly_flutter/Sources/countly_flutter/include/countly_flutter/CountlyFlutterPlugin.h similarity index 100% rename from ios/Classes/CountlyFlutterPlugin.h rename to ios/countly_flutter/Sources/countly_flutter/include/countly_flutter/CountlyFlutterPlugin.h diff --git a/scripts/no-push-files/countly_flutter_np.podspec b/scripts/no-push-files/countly_flutter_np.podspec index aae8fcf1..95f24c2f 100644 --- a/scripts/no-push-files/countly_flutter_np.podspec +++ b/scripts/no-push-files/countly_flutter_np.podspec @@ -9,8 +9,10 @@ Pod::Spec.new do |s| s.social_media_url = 'https://twitter.com/gocountly' s.author = {'Countly' => 'hello@count.ly'} s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/CountlyFlutterPlugin.h' + # Sources live in the Swift Package Manager layout; CocoaPods and SwiftPM share the same files. + s.source_files = 'countly_flutter_np/Sources/countly_flutter_np/**/*.{h,m,swift}' + s.public_header_files = 'countly_flutter_np/Sources/countly_flutter_np/include/countly_flutter_np/CountlyFlutterPlugin.h' + s.resource_bundles = { 'countly_flutter_np_privacy' => ['countly_flutter_np/Sources/countly_flutter_np/countly-sdk-ios/PrivacyInfo.xcprivacy'] } s.dependency 'Flutter' s.swift_version = '5.0' s.ios.deployment_target = '10.0' diff --git a/scripts/script.py b/scripts/script.py index ef3b049a..3749cd96 100644 --- a/scripts/script.py +++ b/scripts/script.py @@ -21,8 +21,8 @@ FILES_TO_ERASE = [ '../android/src/main/java/ly/count/dart/countly_flutter/CountlyMessagingService.java', '../ios/countly_flutter.podspec', - '../ios/Classes/CountlyFLPushNotifications.h', - '../ios/Classes/CountlyFLPushNotifications.m' + '../ios/countly_flutter/Sources/countly_flutter/CountlyFLPushNotifications.h', + '../ios/countly_flutter/Sources/countly_flutter/CountlyFLPushNotifications.m' ] # array of string values. Relative path to the files. Something like: 'android/sth/sth.txt' FILES_TO_MOVE = [ [ @@ -52,7 +52,7 @@ ] # array of, arrays of string tuples. Relative path to the file and the relative path to the copy directory. Something like ['android/sth/sth.txt','android2/folder'] # paths to modify modPathAndroid = '../android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java' -modPathIos = '../ios/Classes/CountlyFlutterPlugin.m' +modPathIos = '../ios/countly_flutter/Sources/countly_flutter/CountlyFlutterPlugin.m' modPathCountly = '../lib/src/countly_flutter.dart' modPathExampleYaml = '../example/pubspec.yaml' # paths to change packages @@ -211,6 +211,67 @@ def update_package(directory_path, from_package, to_package): print(f'File processed: {file_path}') +# Renames the iOS Swift Package Manager package from countly_flutter to countly_flutter_np. +# Flutter discovers a plugin's SwiftPM package at ios//Package.swift, and the +# np flavor's package name is countly_flutter_np, so the package directory, target directory, +# public-header directory, and the names inside Package.swift must all be renamed to match. +# The Objective-C class (CountlyFlutterPlugin) and pluginClass in pubspec are unchanged. +def renameIosForNp(cwd): + iosDir = os.path.join(cwd, '../ios') + oldPkg = os.path.join(iosDir, 'countly_flutter') + newPkg = os.path.join(iosDir, 'countly_flutter_np') + if not os.path.exists(oldPkg): + print('iOS SwiftPM package dir not found, skipping rename:', oldPkg) + return + + # Rename the inner target dir and its public-header dir before renaming the package dir. + srcOld = os.path.join(oldPkg, 'Sources', 'countly_flutter') + incOld = os.path.join(srcOld, 'include', 'countly_flutter') + if os.path.exists(incOld): + os.rename(incOld, os.path.join(srcOld, 'include', 'countly_flutter_np')) + if os.path.exists(srcOld): + os.rename(srcOld, os.path.join(oldPkg, 'Sources', 'countly_flutter_np')) + + # Rewrite the SwiftPM manifest names (package, target, product/library, header search path). + pkgSwift = os.path.join(oldPkg, 'Package.swift') + if os.path.exists(pkgSwift): + with open(pkgSwift, 'r') as f: + content = f.read() + content = content.replace('name: "countly_flutter"', 'name: "countly_flutter_np"') + content = content.replace('["countly_flutter"]', '["countly_flutter_np"]') + content = content.replace('name: "countly-flutter"', 'name: "countly-flutter-np"') + content = content.replace('include/countly_flutter"', 'include/countly_flutter_np"') + with open(pkgSwift, 'w') as f: + f.write(content) + print('Rewrote Package.swift names for countly_flutter_np') + + # The example app's AppDelegate imports the plugin module by name; the np pod's module + # is countly_flutter_np. (GeneratedPluginRegistrant is regenerated by flutter pub get.) + appDelegate = os.path.join(cwd, '../example/ios/Runner/AppDelegate.swift') + if os.path.exists(appDelegate): + with open(appDelegate, 'r') as f: + ad = f.read() + ad = ad.replace('import countly_flutter\n', 'import countly_flutter_np\n') + with open(appDelegate, 'w') as f: + f.write(ad) + print('Updated example AppDelegate import for countly_flutter_np') + + # The example app's Notification Service Extension target references the vendored + # CountlyNotificationService files by path into the plugin; repoint it at the renamed dir. + pbxproj = os.path.join(cwd, '../example/ios/Runner.xcodeproj/project.pbxproj') + if os.path.exists(pbxproj): + with open(pbxproj, 'r') as f: + pbx = f.read() + pbx = pbx.replace('ios/countly_flutter/Sources/countly_flutter/', + 'ios/countly_flutter_np/Sources/countly_flutter_np/') + with open(pbxproj, 'w') as f: + f.write(pbx) + print('Updated example NSE references for countly_flutter_np') + + # Finally rename the package directory itself. + os.rename(oldPkg, newPkg) + print('Renamed iOS SwiftPM package directory to countly_flutter_np') + def main(): # give info about set constants print('Paths to erase:') @@ -253,6 +314,8 @@ def main(): update_package(packagePathLib, packagePrefix, packagePrefixToChange) update_package(packagePathLibInternal, packagePrefix, packagePrefixToChange) update_package(packagePathIntegrationTest, packagePrefix, packagePrefixToChange) + # rename the iOS SwiftPM package to match the np package name + renameIosForNp(cwd) print('Done') else: print('Aborted') diff --git a/scripts/sync_sdk_versions.dart b/scripts/sync_sdk_versions.dart index fc7e8cd9..f1f5b6a6 100644 --- a/scripts/sync_sdk_versions.dart +++ b/scripts/sync_sdk_versions.dart @@ -71,10 +71,10 @@ void main(List args) { ); replaceInFile( - '$rootDir/ios/Classes/CountlyFlutterPlugin.m', + '$rootDir/ios/countly_flutter/Sources/countly_flutter/CountlyFlutterPlugin.m', RegExp(r'kCountlyFlutterSDKVersion = @".+"'), 'kCountlyFlutterSDKVersion = @"$flutterVersion"', - 'Flutter → ios/Classes/CountlyFlutterPlugin.m', + 'Flutter → ios/countly_flutter/Sources/countly_flutter/CountlyFlutterPlugin.m', ); replaceInFile( @@ -123,7 +123,7 @@ void main(List args) { // ---- iOS: submodule init & sparse checkout ---- final iosTag = args.isNotEmpty ? args[0] : iosVersion; - final submodulePath = 'ios/Classes/countly-sdk-ios'; + final submodulePath = 'ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios'; final sparseFile = '$scriptDir/config/sparse-checkout.list'; print(''); @@ -172,13 +172,13 @@ void main(List args) { 'pubspec.yaml', 'ios/countly_flutter.podspec', 'android/src/main/java/ly/count/dart/countly_flutter/CountlyFlutterPlugin.java', - 'ios/Classes/CountlyFlutterPlugin.m', + 'ios/countly_flutter/Sources/countly_flutter/CountlyFlutterPlugin.m', 'lib/src/web/plugin_config.dart', 'scripts/no-push-files/pubspec.yaml', 'scripts/no-push-files/countly_flutter_np.podspec', 'android/build.gradle', 'scripts/no-push-files/build.gradle', - 'ios/Classes/countly-sdk-ios', + 'ios/countly_flutter/Sources/countly_flutter/countly-sdk-ios', ], rootDir); print('✅ All changed files staged');