Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

[quick_actions]Migrate the plugin class to Swift, and remove custom modulemap #6369

Conversation

hellohuanlin
Copy link
Contributor

@hellohuanlin hellohuanlin commented Sep 7, 2022

As suggested in #6229, this PR contains 2 changes together: (1) migration of FLTQuickActionsPlugin class, and (2) removing custom modulemap

About migration of FLTQuickActionsPlugin:

Why migrating this class first

This is following the "top down approach" discussed in the proposal. The migration is pretty smooth - Swift has much better support when importing ObjC.

Why test is not migrated in this PR

Migrating tests in separate PRs will give us better confidence, because the code will be verified by both ObjC and Swift tests, according to the proposal:

"For ObjC code that is already tested, make sure to also write tests in Swift. It is even better if the existing ObjC test just works for the migrated Swift code, because it provides us with good confidence that the new Swift code behaves exactly the same as the original ObjC code. Though due to interoperability and OCMock limitations discussed above, it is not always possible to migrate code and its test in separate steps. "

Swift format

For now I run swift-format on my local machine. But we should add the check to CI in the future, before wider community starts contributing Swift code.

About removing custom modulemap:

I do not like this solution since it makes private headers public. However, I wasn't able to find a better solution after lots of research on this topic.

Below is a summary of what I found so far, in case it's useful searching for a better solution in the future:

  1. Cocoapods does not support a custom modulemap file for static libraries that contains Swift:
[!] Using Swift static libraries with custom module maps is currently not supported. Please build `quick_actions_ios` as a framework or remove the custom module map.

This limitation is the culprit of everything. If we could use a custom modulemap, we could easily define a submodule (aka we are currently doing) or use a private modulemap to map the private headers, like this.

  1. Adding use_frameworks! to the Podfile also works, but we can't force developers to choose frameworks and not static libraries.

  2. There seems to be some effort fixing this problem in Cocoapods (this and this), but there isn't much update since then.

  3. What about removing clang modules at all? In pure ObjC it's fine, but Swift cannot directly import headers - it can only interact with modules.

  4. The auto-generated default modulemap simply exports everything from the umbrella header. It defines a submodule for each of the header import, and re-export them to the main module. Only public headers are allowed in this process. Something like this:

module ModuleName {
  umbrella header "ModuleName-umbrella.h"
  export * 
  module * { export * }
}

This is only a temporary workaround during migration, but the bad state could last a long time for larger plugins. Though on the bright side, consider that private headers aren't that "private" at all - they can be imported by clients just like public headers. it's only that they are put under a separate directory named "PrivateHeaders" to warn the clients that it's risky to import them, but nothing can technically stop them from doing so.

List which issues are fixed by this PR. You must list at least one issue.

flutter/flutter#108750

If you had to change anything in the flutter/tests repo, include a link to the migration guide as per the breaking change policy.

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

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

@hellohuanlin hellohuanlin force-pushed the quick_actions_remove_custom_module_map_and_migration_plugin_class branch from 31335dd to 40875c9 Compare September 7, 2022 01:19

- (void)dealloc {
[_channel setMethodCallHandler:nil];
_channel = nil;
Copy link
Contributor Author

@hellohuanlin hellohuanlin Sep 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not migrate this function since I don't think it's necessary. _channel should be gone automatically. Also double checked other plugins and they don't call setMethodCallHandler:nil

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also double checked other plugins and they don't call setMethodCallHandler:nil

It's been a while since I dug into plugin lifetime, but IIRC it's the channel handler registration that keeps the plugin instance alive in the first place; if that's true, then this will never do anything in dealloc since we couldn't be here if there's still an active channel. I may be misremembering though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. This would be a serious memory issue for our plugins. I created flutter/flutter#111206 to track it.

@hellohuanlin hellohuanlin force-pushed the quick_actions_remove_custom_module_map_and_migration_plugin_class branch from 40875c9 to d54408e Compare September 7, 2022 01:59
@hellohuanlin hellohuanlin force-pushed the quick_actions_remove_custom_module_map_and_migration_plugin_class branch from d54408e to f5b80bc Compare September 7, 2022 03:45
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
}
s.public_header_files = 'Classes/**/*.h'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately we have to expose private headers until migration is complete (refer to the PR description)

fix unused variable warnings

nit
@hellohuanlin hellohuanlin force-pushed the quick_actions_remove_custom_module_map_and_migration_plugin_class branch from 479b98a to ef032a4 Compare September 7, 2022 18:51
switch call.method {
case "setShortcutItems":
// `arguments` must be an array of dictionaries
let items = call.arguments as! [[String: Any]]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force unwrap it to keep the same behavior as ObjC. Whether to crash on platform code or to surface an error is discussed here in this proposal.

Copy link
Contributor

@cyanglaz cyanglaz Sep 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If items is nil, it doesn't crash on Objective-C(here). It actually sets the [UIApplication sharedApplication].shortcutItems to an empty array, which results the same behavior as "clearShortcutItems". On the other hand, it would crash in this swift version due to force unwrapping.

I think the current Objective-C behavior is more like?

let items = call.arguments as [[String: Any]] ?? []
shortcutStateManager.setShortcutItems(items)
result(nil)

I'm not sure the original design of an implicit "clearShortcutItems" is intended but for migration I feel like we should keep the behavior the same if possible and have a follow up PR to handle the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If wrong type it should crash:

NSNumber *arr = @1;
for (NSDictionary *item in (NSArray *)arr) { // crash here
  NSLog(@"%@", item);
}

But I see your point about nil value. The equivalent swift should be this instead:

if let arguments = call.arguments {
  // crash of wrong type
  items = arguments as! [[String:Any]]
} else {
  // no crash if nil
  items = []
}

Not sure if it's worth to do tho. The corresponding dart side guarantees that the argument is non-null. So the else block actually never run.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

// Return false to indicate we handled the quick action to ensure
// the `application:performActionFor:` method is not called (as
// per Apple's documentation:
// https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these comments are copied over from objc.

@hellohuanlin hellohuanlin marked this pull request as ready for review September 7, 2022 18:57
@stuartmorgan
Copy link
Contributor

For now I run swift-format on my local machine. But we should add the check to CI in the future, before wider community starts contributing Swift code.

I don't think that's viable yet for the reasons in flutter/flutter#41129 (comment). We already have macOS plugins that are using Swift, and we accept changes to them. Auto-formatting is definitely convenient, but it's not catastrophic if we don't have it.

We should add support for it in the tool sooner rather than later, and provide instructions in the tooling docs for using it; then if we have PRs that have really problematic formatting we can point people to those to do locally. (Minor drift relative to the autoformatter is something we can just automatically fix as people who do run the autoformatter change files.)


- (void)dealloc {
[_channel setMethodCallHandler:nil];
_channel = nil;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also double checked other plugins and they don't call setMethodCallHandler:nil

It's been a while since I dug into plugin lifetime, but IIRC it's the channel handler registration that keeps the plugin instance alive in the first place; if that's true, then this will never do anything in dealloc since we couldn't be here if there's still an active channel. I may be misremembering though.

@@ -13,7 +13,7 @@ flutter:
implements: quick_actions
platforms:
ios:
pluginClass: FLTQuickActionsPlugin
pluginClass: QuickActionsPlugin
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we still require the thin ObjC wrapper around Swift plugins? That's still what we are generating in the template; I thought it was required for the Swift-plugin-in-ObjC host to work in iOS apps. @jmagman

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated file is this:

#import "GeneratedPluginRegistrant.h"

#if __has_include(<integration_test/IntegrationTestPlugin.h>)
#import <integration_test/IntegrationTestPlugin.h>
#else
@import integration_test;
#endif

#if __has_include(<quick_actions_ios/QuickActionsPlugin.h>)
#import <quick_actions_ios/QuickActionsPlugin.h>
#else
@import quick_actions_ios;
#endif

@implementation GeneratedPluginRegistrant

+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
  [IntegrationTestPlugin registerWithRegistrar:[registry registrarForPlugin:@"IntegrationTestPlugin"]];
  [QuickActionsPlugin registerWithRegistrar:[registry registrarForPlugin:@"QuickActionsPlugin"]];
}

@end

It is directly using [QuickActionsPlugin registerWithRegistrar:]. So we should be fine here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmagman Did __has_include resolve all the issues with direct Swift-into-Obj-C import? If so, we should fix the plugin template to stop generating the Obj-C wrapper.

Copy link
Contributor Author

@hellohuanlin hellohuanlin Sep 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting to remove GeneratedPluginRegistrant.h/m and move the logic to AppDelegate?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I'm talking about the ObjC files here.

It seems like either there is still at least one combination of app language and library build type that requires those wrappers, in which case this plugin needs them too, or that's no longer the case, in which case the template shouldn't need them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objc-plugin-in-Swift-app will work without the wrapper if the Objective-C plugins define modules correctly, which they should out of the box as of plugin podspec template change in flutter/flutter#40302 (Flutter 1.10.3). Plugins created before that point may not work when used in a Swift app since it's missing DEFINES_MODULE. I think all other combinations should work without the wrapper.

We could add a warning to the tool for the plugin author that they need to add { 'DEFINES_MODULE' => 'YES' } to their podspec (although I don't think we have a good tool hook to know they are building a plugin example app and not just a regular app). Or we could add some documentation about how to fix up the podspec when their plugin customers report it doesn't work in a Swift app.

Copy link
Contributor Author

@hellohuanlin hellohuanlin Sep 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmagman thanks for the explanation. Sounds like we should remove those templates. Created an issue here flutter/flutter#111928

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objc-plugin-in-Swift-app will work without the wrapper if the Objective-C plugins define modules correctly

Are all the languages swapped in this comment by accident, or we talking about different cases? I was talking about the ObjC wrapper that we generate in the Swift plugin template, to make them look like older (ObjC) plugins, which IIRC was necessary for the Swift-plugin-in-ObjC-app case (for at least one of the library build modes). But I guess modules could fix mismatches in both directions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all the languages swapped in this comment by accident, or we talking about different cases? I was talking about the ObjC wrapper that we generate in the Swift plugin template, to make them look like older (ObjC) plugins, which IIRC was necessary for the Swift-plugin-in-ObjC-app case (for at least one of the library build modes).

You're right I was mixing this up with the Objective-C files that get generated in a Swift iOS app, which I think was originally so the Swift app can import Objective-C plugins pre-modules.
https://github.com/flutter/flutter/blob/master/packages/flutter_tools/templates/app_shared/ios-swift.tmpl/Runner/Runner-Bridging-Header.h
https://github.com/flutter/flutter/blob/5aad12cab1e9c0863ea5fbc9a770b2734d7772db/packages/flutter_tools/lib/src/flutter_plugins.dart#L456

Screen Shot 2022-09-19 at 3 58 35 PM

But you're right, I think the Swift plugins also don't need the Objective-C code now. Fortunately this area is pretty well tested, so if someone tries flutter/flutter#111928 the tests should let us know if there are any corner cases I'm not remembering.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading it again (@jmagman you gave me hints about clang module), i think it's possible that the original plugin template wanted to expose both header and module, so that its objc client can freely choose from importing headers vs importing modules. This benefit seems so trivial that we should probably remove the template.

Copy link
Contributor

@cyanglaz cyanglaz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swift code LGTM

Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jmagman
Copy link
Member

jmagman commented Sep 21, 2022

@hellohuanlin is this ready to land?

@hellohuanlin
Copy link
Contributor Author

@jmagman sorry i was busy with the video player ios 16 bug. will rebase it to fix the submit-queue and land later.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: quick_actions platform-ios
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants