flutter icon indicating copy to clipboard operation
flutter copied to clipboard

Add Swift Package Manager as new opt-in feature for iOS and macOS

Open vashworth opened this issue 1 year ago • 7 comments

This PR adds initial support for Swift Package Manager (SPM). Users must opt in. Only compatible with Xcode 15+.

Fixes https://github.com/flutter/flutter/issues/146369.

Included Features

This PR includes the following features:

  • Enabling SPM via config flutter config --enable-swift-package-manager
  • Disabling SPM via config (will disable for all projects) flutter config --no-enable-swift-package-manager
  • Disabling SPM via pubspec.yaml (will disable for the specific project)
flutter:
  disable-swift-package-manager: true
  • Migrating existing apps to add SPM integration if using a Flutter plugin with a Package.swift
    • Generates a Swift Package (named FlutterGeneratedPluginSwiftPackage) that handles Flutter SPM-compatible plugin dependencies. Generated package is added to the Xcode project.
  • Error parsing of common errors that may occur due to using CocoaPods and Swift Package Manager together
  • Tool will print warnings when using all Swift Package plugins and encourage you to remove CocoaPods

This PR also converts integration_test and integration_test_macos plugins to be both Swift Packages and CocoaPod Pods.

How it Works

The Flutter CLI will generate a Swift Package called FlutterGeneratedPluginSwiftPackage, which will have local dependencies on all Swift Package compatible Flutter plugins.

The FlutterGeneratedPluginSwiftPackage package will be added to the Xcode project via altering of the project.pbxproj.

In addition, a "Pre-action" script will be added via altering of the Runner.xcscheme. This script will invoke the flutter tool to copy the Flutter/FlutterMacOS framework to the BUILT_PRODUCTS_DIR directory before the build starts. This is needed because plugins need to be linked to the Flutter framework and fortunately Swift Package Manager automatically uses BUILT_PRODUCTS_DIR as a framework search path.

CocoaPods will continue to run and be used to support non-Swift Package compatible Flutter plugins.

Not Included Features

It does not include the following (will be added in future PRs):

  • Create plugin template
  • Create app template
  • Add-to-App integration

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

vashworth avatar Apr 04 '24 03:04 vashworth

Instructions on how to use Swift Package Manager - WIP (Not final)

Enable Swift Package Manager

Enable Swift Package Manager

Run the following command:

flutter config --enable-swift-package-manager
Disable Swift Package Manager

Disable Swift Package Manager

Disabling Swift Package Manager will cause Flutter to use CocoaPods for all dependencies. However, Swift Package Manager will remain intregrated with your project. To remove integration, follow "How to remove Swift Package Manager integration" instructions below.

Disable for a single project

In the project's pubspec.yaml, under the flutter section, add disable-swift-package-manager: true.

# The following section is specific to Flutter packages.
flutter:
  disable-swift-package-manager: true

Disable globally for all projects

Run the following command:

flutter config --no-enable-swift-package-manager
Converting an existing Objective-C Flutter Plugin to a Swift Package

Converting an existing Objective-C Flutter Plugin to a Swift Package

Replace plugin_name throughout this guide with the name of your plugin.

  1. Start by creating a directory under the ios, macos, and/or darwin directories. Name this new directory the name of the platform package. The below example uses ios, replace ios with macos/darwin if applicable.
/plugin_name/plugin_name_ios/ios/plugin_name_ios
  1. Within this new directory, create the following files/directories:
    • Package.swift (file)
    • Sources (directory)
    • Sources/plugin_name_ios (directory)
    • Sources/plugin_name_ios/include (directory)
    • Sources/plugin_name_ios/include/plugin_name_ios (directory)
    • Sources/plugin_name_ios/include/plugin_name_ios/.gitkeep (file)
      • Needed to ensure the directory is committed, even if empty. Can be removed if files are added to the directory.
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Package.swift
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources/plugin_name_ios
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources/plugin_name_ios/include
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources/plugin_name_ios/include/plugin_name_ios
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources/plugin_name_ios/include/plugin_name_ios/.gitkeep
  1. Use the following template in the Package.swift
// 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: "plugin_name_ios",
    platforms: [
        .iOS("12.0"),
        .macOS("10.14")
    ],
    products: [
        // If the plugin name contains "_", replace with "-" for the library name
        .library(name: "plugin-name-ios", targets: ["plugin_name_ios"])
    ],
    dependencies: [],
    targets: [
        .target(
            name: "plugin_name_ios",
            dependencies: [],
            resources: [
                // If your plugin requires a privacy manifest, for example if it uses any required
                // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
                // privacy impact, and then uncomment these lines. For more information, see
                // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
                // .process("PrivacyInfo.xcprivacy"),

                // If you have other resources that need to be bundled with your plugin, refer to
                // the following instructions to add them:
                // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
            ],
            cSettings: [
                .headerSearchPath("include/plugin_name_ios")
            ]
        )
    ]
)
  • If the plugin name contains _, the library name must be a - separated version of the plugin name.
  1. If your plugin has a PrivacyInfo.xcprivacy, move it to Sources/plugin_name_ios/PrivacyInfo.xcprivacy and uncomment the resource in the Package.swift.
            resources: [
                // If your plugin requires a privacy manifest, for example if it uses any required
                // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
                // privacy impact, and then uncomment these lines. For more information, see
                // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
-                // .process("PrivacyInfo.xcprivacy"),
+                .process("PrivacyInfo.xcprivacy"),

                // If you have other resources that need to be bundled with your plugin, refer to
                // the following instructions to add them:
                // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
            ],
  1. Move any resource files from ios/Assets to Sources/plugin_name_ios (or a subdirectory). Then add them to your Package.swift if applicable. See https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package for more instructions.

  2. Move any public headers from ios/Classes to Sources/plugin_name_ios/include/plugin_name_ios

    • If you're unsure what headers are public, check your podspec for public_header_files. If not found, that means all of your headers were public. You should consider whether or not you want all of your headers to be public.
    • The pluginClass defined in your pubspec.yaml must be public and within this directory.
  3. Handling modulemap (skip this step if not using a custom modulemap)

    If you're using a modulemap for CocoaPods to create a Test submodule, consider removing it for Swift Package Manager. Note that this will make all public headers available via the module.

    To remove it for Swift Package Manager but keep it for CocoaPods, exclude the modulemap and umbrella header in the plugin's Package.swift. The example below assumes they are located within the Sources/plugin_name_ios directory.

            .target(
                name: "plugin_name_ios",
                dependencies: [],
    +           exclude: ["cocoapods_plugin_name_ios.modulemap", "plugin_name_ios-umbrella.h"],
    

    If you want to keep your unit tests compatible with both CocoaPods and Swift Package Manager, you can try the following:

    @import plugin_name_ios;
    - @import plugin_name_ios.Test;
    + #if __has_include(<plugin_name_ios/plugin_name_ios-umbrella.h>)
    +   @import plugin_name_ios.Test;
    + #endif
    

    If you would like to continue using a custom modulemap, please refer to Swift Package Manager's documentation.

  4. Move all remaining files from ios/Classes to Sources/plugin_name_ios

  5. ios/Assets, ios/Resources, ios/Classes should now be empty and can be deleted

  6. If your header files were previously within the same directory as your implementation files, you may need to change your import statements.

For example, if the following changes were made: * ios/Classes/PublicHeaderFile.h --> Sources/plugin_name_ios/include/plugin_name_ios/PublicHeaderFile.h * ios/Classes/ImplementationFile.m --> Sources/plugin_name_ios/ImplementationFile.m

Within ImplementationFile.m, the import would change:

- #import "PublicHeaderFile.h"
+ #import "./include/plugin_name_ios/PublicHeaderFile.h"
  1. If using pigeon, you'll want to update your pigeon input file
- objcHeaderOut: 'ios/Classes/messages.g.h',
+ objcHeaderOut: 'ios/plugin_name_ios/Sources/plugin_name_ios/messages.g.h',
- objcSourceOut: 'ios/Classes/messages.g.m',
+ objcSourceOut: 'ios/plugin_name_ios/Sources/plugin_name_ios/messages.g.m',

If your objcHeaderOut file is no longer within the same directory as the objcSourceOut, you can change the #import using ObjcOptions.headerIncludePath:

objcHeaderOut: 'ios/plugin_name_ios/Sources/plugin_name_ios/include/plugin_name_ios/messages.g.h',
objcSourceOut: 'ios/plugin_name_ios/Sources/plugin_name_ios/messages.g.m',
+ objcOptions: ObjcOptions(
+   headerIncludePath: './include/plugin_name_ios/messages.g.h',
+ ),
  1. Update your Package.swift with any customizations you may need

    1. Open /plugin_name/plugin_name_ios/ios/plugin_name_ios/ in Xcode
      • If package does not show any files in Xcode, quit Xcode (Xcode > Quit Xcode) and reopen
      • You don't need to edit your Package.swift through Xcode, but Xcode will provide helpful feedback
      • If Xcode isn't updating after you make a change, try clicking File > Packages > Reset Package Caches
    2. Add dependencies
    3. If your package must be linked explicitly static or dynamic (not recommended), update the Product to define the type
    products: [
        .library(name: "plugin-name-ios", type: .static, targets: ["plugin_name_ios"])
    ],
    
    1. Make any other customizations - see https://developer.apple.com/documentation/packagedescription for more info on how to write a Package.swift.
    2. If you add additional targets to your Package.swift, try to name them uniquely. If your target name conflicts with another target from another package, this can cause issues that may require manual intervention to be able to use your plugin.
  2. Update your plugin_name_ios.podspec to point to new paths.

- s.source_files = 'Classes/**/*.{h,m}'
+ s.source_files = 'plugin_name_ios/Sources/plugin_name_ios/**/*.{h,m}'

- s.public_header_files = 'Classes/**/*.h'
+ s.public_header_files = 'plugin_name_ios/Sources/plugin_name_ios/include/**/*.h'

- s.module_map = 'Classes/cocoapods_plugin_name_ios.modulemap'
+ s.module_map = 'plugin_name_ios/Sources/plugin_name_ios/include/cocoapods_plugin_name_ios.modulemap'

- s.resource_bundles = {'plugin_name_ios_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
+ s.resource_bundles = {'plugin_name_ios_privacy' => ['plugin_name_ios/Sources/plugin_name_ios/PrivacyInfo.xcprivacy']}
  1. Update getting of resources from bundle to use SWIFTPM_MODULE_BUNDLE
#if SWIFT_PACKAGE
   NSBundle *bundle = SWIFTPM_MODULE_BUNDLE;
 #else
   NSBundle *bundle = [NSBundle bundleForClass:[self class]];
 #endif
 NSURL *imageURL = [bundle URLForResource:@"image" withExtension:@"jpg"];
  1. If your plugin_name_ios/Sources/plugin_name_ios/include directory only contains a .gitkeep, you'll want update your .gitignore to include the following:
!.gitkeep

Then run flutter pub publish --dry-run to ensure the include directory will be published.

  1. Verify plugin still works with CocoaPods

    1. Disable Swift Package Manager
    flutter config --no-enable-swift-package-manager
    
    1. Run flutter run with the example app and ensure it builds and runs
  2. Verify plugin works with Swift Package Manager

    1. Enable Swift Package Manager
    flutter config --enable-swift-package-manager
    
    1. Run flutter run with the example app and ensure it builds and runs
    2. Open the example app in Xcode and ensure Package Dependencies show in the left Project Navigator
  3. Verify tests pass

Converting an existing Swift Flutter Plugin to a Swift Package

Converting an existing Swift Flutter Plugin to a Swift Package

Replace plugin_name throughout this guide with the name of your plugin.

  1. Start by creating a directory under the ios, macos, and/or darwin directories. Name this new directory the name of the platform package. The below example uses ios, replace ios with macos/darwin if applicable.
/plugin_name/plugin_name_ios/ios/plugin_name_ios
  1. Within this new directory, create the following files/directories:
    • Package.swift (file)
    • Sources (directory)
    • Sources/plugin_name_ios (directory)
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Package.swift
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources
/plugin_name/plugin_name_ios/ios/plugin_name_ios/Sources/plugin_name_ios
  1. Use the following template in the Package.swift
// 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: "plugin_name_ios",
    platforms: [
        .iOS("12.0"),
        .macOS("10.14")
    ],
    products: [
        // If the plugin name contains "_", replace with "-" for the library name
        .library(name: "plugin-name-ios", targets: ["plugin_name_ios"])
    ],
    dependencies: [],
    targets: [
        .target(
            name: "plugin_name_ios",
            dependencies: [],
            resources: [
                // If your plugin requires a privacy manifest, for example if it uses any required
                // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
                // privacy impact, and then uncomment these lines. For more information, see
                // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
                // .process("PrivacyInfo.xcprivacy"),

                // If you have other resources that need to be bundled with your plugin, refer to
                // the following instructions to add them:
                // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
            ]
        )
    ]
)
  • If the plugin name contains _, the library name must be a - separated version of the plugin name.
  1. If your plugin has a PrivacyInfo.xcprivacy, move it to Sources/plugin_name_ios/PrivacyInfo.xcprivacy and uncomment the resource in the Package.swift.
            resources: [
                // If your plugin requires a privacy manifest, for example if it uses any required
                // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
                // privacy impact, and then uncomment these lines. For more information, see
                // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
-                // .process("PrivacyInfo.xcprivacy"),
+                .process("PrivacyInfo.xcprivacy"),

                // If you have other resources that need to be bundled with your plugin, refer to
                // the following instructions to add them:
                // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
            ],
  1. Move any resource files from ios/Assets to Sources/plugin_name_ios (or a subdirectory). Then add them to your Package.swift if applicable. See https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package for more instructions.
  2. Move all files from ios/Classes to Sources/plugin_name_ios
  3. ios/Assets, ios/Resources, ios/Classes should now be empty and can be deleted
  4. If using pigeon, you'll want to update your pigeon input file
- swiftOut: 'ios/Classes/messages.g.swift',
+ swiftOut: 'ios/plugin_name_ios/Sources/plugin_name_ios/messages.g.swift',
  1. Update your Package.swift with any customizations you may need
    1. Open /plugin_name/plugin_name_ios/ios/plugin_name_ios/ in Xcode
      • If package does not show any files in Xcode, quit Xcode (Xcode > Quit Xcode) and reopen
      • You don't need to edit your Package.swift through Xcode, but Xcode will provide helpful feedback
      • If Xcode isn't updating after you make a change, try clicking File > Packages > Reset Package Caches
    2. Add dependencies
    3. If your package must be linked explicitly static or dynamic, update the Product to define the type
    products: [
        .library(name: "plugin-name-ios", type: .static, targets: ["plugin_name_ios"])
    ],
    
    1. Make any other customizations - see https://developer.apple.com/documentation/packagedescription for more info on how to write a Package.swift.
    2. If you add additional targets to your Package.swift, try to name them uniquely. If your target name conflicts with another target from another package, this can cause issues that may require manual intervention to be able to use your plugin.
  2. Update your plugin_name_ios.podspec to point to new paths.
- s.source_files = 'Classes/**/*.swift'
+ s.source_files = 'plugin_name_ios/Sources/plugin_name_ios/**/*.swift'

- s.resource_bundles = {'plugin_name_ios_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
+ s.resource_bundles = {'plugin_name_ios_privacy' => ['plugin_name_ios/Sources/plugin_name_ios/PrivacyInfo.xcprivacy']}
  1. Update getting of resources from bundle to use Bundle.module
#if SWIFT_PACKAGE
     let settingsURL = Bundle.module.url(forResource: "image", withExtension: "jpg")
#else
     let settingsURL = Bundle(for: Self.self).url(forResource: "image", withExtension: "jpg")
#endif
  1. Verify plugin still works with CocoaPods
    1. Disable Swift Package Manager
    flutter config --no-enable-swift-package-manager
    
    1. Run flutter run with the example app and ensure it builds and runs
  2. Verify plugin works with Swift Package Manager
    1. Enable Swift Package Manager
    flutter config --enable-swift-package-manager
    
    1. Run flutter run with the example app and ensure it builds and runs
    2. Open the example app in Xcode and ensure Package Dependencies show in the left Project Navigator
  3. Verify tests pass
Updating unit tests in plugin example app

Updating unit tests in plugin example app

If your plugin has native XCTests, you may need to update them to work with Swift Package Manger if one of the following is true:

  • You're using a CocoaPod dependency for the test
  • Your plugin is explicitly set to type: .dynamic in its Package.swift
  1. Open your example/ios/Runner.xcworkspace in Xcode
  2. If you were using a CocoaPod dependency for tests, such as OCMock, you'll want to remove it from your Podfile
target 'RunnerTests' do
  inherit! :search_paths
-  pod 'OCMock', '3.5'
end`

Then in the terminal, run pod install in the plugin_name_ios/example/ios directory

  1. Navigate to Package Dependencies for the project

Screenshot 2024-04-05 at 10 13 56 AM

  1. Click the + button and add any test-only dependencies by searching for them in the top right search bar.

Screenshot 2024-04-09 at 3 11 21 PM

Note: OCMock uses unsafe build flags and cant only be used if targeted by commit. fe1661a3efed11831a6452f4b1a0c5e6ddc08c3d is the commit for the 3.9.3 version.

  1. Ensure it is added to the RunnerTests Target and click Add Package button

Screenshot 2024-04-09 at 3 12 12 PM

  1. If you've explicitly set your plugin's library type to .dynamic in its Package.swift (not recommended), you'll also need to add it as a dependency to the RunnerTests target.

    1. First, ensure RunnerTests has a Link Binary With Libraries Build Phase Screenshot 2024-04-19 at 3 14 56 PM

    2. If it does not already exist, create one by selecting the + button and selecting New Link Binary With Libraries Phase Screenshot 2024-04-19 at 3 13 01 PM

    3. Navigate to Package Dependencies for the project

    4. Click the + button

    5. Click the Add Local... button on the bottom of the dialog that opens

    6. Navigate to plugin_name/plugin_name_ios/ios/plugin_name_ios and click Add Package button

    7. Ensure it is added to the RunnerTests Target and click Add Package button

  2. Ensure tests pass Product > Test

How to manually add Swift Package Manager integration to iOS project if Flutter CLI fails to automatically migrate

How to manually add Swift Package Manager integration to iOS project if Flutter CLI fails to automatically migrate

Please file a bug before manually migrating to help the Flutter team improve the automatic migration. Please include the error message you received and consider including a copy of the of the following files in your bug report:

  • ios/Runner.xcodeproj/project.pbxproj
  • ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (or the xcsheme for the flavor used)

Part 1: Add FlutterGeneratedPluginSwiftPackage Package Dependency

  1. Open your app (your_app/ios/Runner.xcworkspace) in Xcode
  2. Navigate to Package Dependencies for the project

Screenshot 2024-04-05 at 10 13 56 AM

  1. Click the + button
  2. Click the Add Local... button on the bottom of the dialog that opens
  3. Navigate to your_app/ios/Flutter/Packages/FlutterGeneratedPluginSwiftPackage and click Add Package button
  4. Ensure it is added to the Runner Target and click Add Package button

Screenshot 2024-04-05 at 10 17 21 AM

  1. Ensure FlutterGeneratedPluginSwiftPackage was added to Frameworks, Libraries, and Embedded Content

Screenshot 2024-04-05 at 10 20 12 AM

Part 2: Add Run Prepare Flutter Framework Script Pre-Action

The following must be completed for each flavor.

  1. Next, select Product > Scheme > Edit Scheme
  2. Click the > next to "Build" in the left side bar
  3. Select Pre-actions
  4. Click the + button and select New Run Script Action from the menu
  5. Click the "Run Script" title and change to Run Prepare Flutter Framework Script.
  6. Change the "Provide build settings from" to the app.
  7. Input the following in the text box:
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare

Screenshot 2024-04-05 at 10 24 44 AM

Part 3: Run app

  1. Run the app in Xcode and ensure FlutterGeneratedPluginSwiftPackage is a target dependency and Run Prepare Flutter Framework Script is being run as a pre-action.

Screenshot 2024-04-05 at 12 31 43 PM

  1. Also, ensure the app runs on the command line with flutter run.
How to manually add Swift Package Manager integration to macOS project if Flutter CLI fails to automatically migrate

How to manually add Swift Package Manager integration to macOS project if Flutter CLI fails to automatically migrate

Please file a bug before manually migrating to help the Flutter team improve the automatic migration. Please include the error message you received and consider including a copy of the of the following files in your bug report:

  • macos/Runner.xcodeproj/project.pbxproj
  • macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (or the xcscheme for the flavor used)

Part 1: Add FlutterGeneratedPluginSwiftPackage Package Dependency

  1. Open your app (your_app/macos/Runner.xcworkspace) in Xcode
  2. Navigate to Package Dependencies for the project

Screenshot 2024-04-05 at 10 13 56 AM

  1. Click the + button
  2. Click the Add Local... button on the bottom of the dialog that opens
  3. Navigate to your_app/macos/Flutter/Packages/FlutterGeneratedPluginSwiftPackage and click Add Package button
  4. Ensure it is added to the Runner Target and click Add Package button

Screenshot 2024-04-05 at 10 17 21 AM

  1. Ensure FlutterGeneratedPluginSwiftPackage was added to Frameworks, Libraries, and Embedded Content

Screenshot 2024-04-05 at 10 20 12 AM

Part 2: Add Run Prepare Flutter Framework Script Pre-Action

The following must be completed for each flavor.

  1. Next, select Product > Scheme > Edit Scheme
  2. Click the > next to "Build" in the left side bar
  3. Select Pre-actions
  4. Click the + button and select New Run Script Action from the menu
  5. Click the "Run Script" title and change to Run Prepare Flutter Framework Script.
  6. Change the "Provide build settings from" to the Runner target.
  7. Input the following in the text box:
"$FLUTTER_ROOT"/packages/flutter_tools/bin/macos_assemble.sh prepare

Screenshot 2024-04-05 at 2 22 56 PM

Part 3: Run app

  1. Run the app in Xcode and ensure FlutterGeneratedPluginSwiftPackage is a target dependency and Run Prepare Flutter Framework Script is being run as a pre-action.

Screenshot 2024-04-05 at 12 31 43 PM

  1. Also, ensure the app runs on the command line with flutter run.
How to use a Swift Package Flutter plugin that requires a higher OS version

If a Swift Package Flutter plugin requires a higher OS version than the project, you may get an error like this:

Target Integrity (Xcode): The package product 'plugin_name_ios' requires minimum platform version 14.0 for the iOS platform, but this target supports 12.0

To still be able to use the plugin, you'll need to increase the Minimum Deployment of your project to match. Keep in mind, this will increase the minimum OS version that your app can run on.

Screenshot 2024-04-05 at 3 04 09 PM

How to add Swift Package Manager integration to a custom target

How to add Swift Package Manager integration to a custom target

Follow the steps in How to manually add Swift Package Manager integration to iOS/macOS project if Flutter CLI fails to automatically migrate.

In Part 1, Step 6 use your custom target instead of the Flutter target.

In Part 2, Step 6 use your custom target instead of the Flutter target.

How to remove Swift Package Manager integration

How to remove Swift Package Manager integration

  1. Disable Swift Package Manager (see "Disable Swift Package Manager" instructions above).
  2. Open your app (your_app/ios/Runner.xcworkspace) in Xcode
  3. Navigate to Package Dependencies for the project
  4. Click on the FlutterGeneratedPluginSwiftPackage package and then click the - button

Screenshot 2024-04-05 at 2 24 48 PM

  1. Navigate to Frameworks, Libraries, and Embedded Content for the Runner target
  2. Click on FlutterGeneratedPluginSwiftPackage and then click the - button

Screenshot 2024-04-05 at 2 25 25 PM

FAQS

Coming soon...

vashworth avatar Apr 05 '24 01:04 vashworth

  • Disabling SPM via pubspec.yaml (will disable for the specific project)

What's the use case for project-level control? It would be nice if we could minimize the number of knobs and switches that we have to support.

stuartmorgan-g avatar Apr 05 '24 18:04 stuartmorgan-g

  • Disabling SPM via pubspec.yaml (will disable for the specific project)

What's the use case for project-level control? It would be nice if we could minimize the number of knobs and switches that we have to support.

By default, it'll use CocoaPods and Swift Package Manager together, but they don't actually work together. For example, if a SPM-supported plugin and CocoaPod-only plugin is being used and both plugins have a same transitive dependency, it can cause errors about duplicate symbols or redefined modules. The only workaround then is to remove one of the plugins or fallback to just using CocoaPods only (aka via disabling SPM in the pubspec.yaml). Hypothetically could disable it globally, but that would be annoying to have to run the enable/disable command per project.

vashworth avatar Apr 05 '24 18:04 vashworth

Ah right, I forgot about that limitation. Makes sense!

stuartmorgan-g avatar Apr 05 '24 18:04 stuartmorgan-g

I converted url_launcher_ios following your excellent instructions (one minor issue, but that was entirely due to me missing part of a step), and it worked in a test project flawlessly. Manually verified end-to-end plugin functionality, and no Pods created!

stuartmorgan-g avatar Apr 08 '24 15:04 stuartmorgan-g

@loic-sharma fyi if you're interested in the first draft of this.

jmagman avatar Apr 08 '24 20:04 jmagman

@stuartmorgan I had to make a couple updates to the instructions.

  1. Library names in the Package.swift manifest can't contain underscores, so I changed them to be hyphen separated.

    Reason: If the package is linked dynamically, SPM automatically uses the library name as the CFBundleIdentifier.

    The bundle ID string must contain only alphanumeric characters (A–Z, a–z, and 0–9), hyphens (-), and periods (.)

    https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier

    Apparently, it will cause error when you try to upload to app store: https://stackoverflow.com/questions/62477747/swift-package-manager-problem-with-bundle-ids

  2. I decided to change the way we do units tests back to using modulemaps. Using a package's testTarget directly doesn't launch a simulator and install the app, and some of our tests assume that it does.

    Also it seems some tests may require permissions from Info.plist, entitlements, etc which is again part of the app.

    Also, this keeps any test-specific dependencies (like OCMock) out of the plugin itself - which will improve speeds to users consuming the plugin since it won’t download extra unneeded dependencies. 


~~I updated the instructions to reflect these new changes, could you try it again when you have time? https://github.com/flutter/flutter/pull/146256#issuecomment-2038565077~~ JK, not ready for re-review yet

vashworth avatar Apr 08 '24 20:04 vashworth

@vashworth I migrated file_selector_ios and file_selector_macos with the new instructions, and everything worked!

(I kept the weird structure where the headers that are supposed to be test-only are actually public; I don't remember why exactly we have to do that for our modulemap-based plugins, but it's a pre-existing problem, and longer term Swift migrations will eliminate the issue so I didn't worry about it.)

stuartmorgan-g avatar Apr 11 '24 16:04 stuartmorgan-g

+1 to that! There's good test coverage so we're well positioned to iterate, all of my manual tests went smoothly, and we probably won't find weird edge-case bugs until we actually have some real-worth early adopters trying it.

Once it lands we can migrate our own plugins ASAP (I have a couple done locally, and should have time this week to do more), to accelerate real-world trials.

stuartmorgan-g avatar Apr 16 '24 23:04 stuartmorgan-g

Once it lands we can migrate our own plugins ASAP (I have a couple done locally, and should have time this week to do more), to accelerate real-world trials.

@stuartmorgan I created https://github.com/flutter/flutter/issues/146922 to track migrating our plugins. I assigned you the ones you mentioned you've already worked on. I assigned myself ones I've already worked on. Once this lands, feel free to migrate any of them - just assign yourself so we don't duplicate work

vashworth avatar Apr 17 '24 17:04 vashworth

Thanks, I was going to make that bugtree myself but you beat me to it :)

stuartmorgan-g avatar Apr 17 '24 18:04 stuartmorgan-g