From b3970225b80183548ecc5516a5c1a1ad61016860 Mon Sep 17 00:00:00 2001 From: Rafal Wachol Date: Thu, 17 Oct 2024 05:49:43 +0900 Subject: [PATCH] feat(share_plus): Add Swift Package Manager support (#3169) Co-authored-by: Volodymyr Buberenko Co-authored-by: Volodymyr --- packages/share_plus/share_plus/.gitignore | 2 + .../share_plus/ios/share_plus.podspec | 7 +- .../share_plus/ios/share_plus/Package.swift | 27 + .../Sources/share_plus/FPPSharePlusPlugin.m | 467 ++++++++++++++++++ .../Sources/share_plus}/PrivacyInfo.xcprivacy | 0 .../include/share_plus/FPPSharePlusPlugin.h | 8 + .../share_plus/macos/share_plus.podspec | 4 +- .../share_plus/macos/share_plus/Package.swift | 24 + .../Sources/share_plus}/PrivacyInfo.xcprivacy | 2 - .../share_plus}/SharePlusMacosPlugin.swift | 0 10 files changed, 533 insertions(+), 8 deletions(-) create mode 100644 packages/share_plus/share_plus/ios/share_plus/Package.swift create mode 100644 packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m rename packages/share_plus/share_plus/{macos => ios/share_plus/Sources/share_plus}/PrivacyInfo.xcprivacy (100%) create mode 100644 packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/include/share_plus/FPPSharePlusPlugin.h create mode 100644 packages/share_plus/share_plus/macos/share_plus/Package.swift rename packages/share_plus/share_plus/{ios => macos/share_plus/Sources/share_plus}/PrivacyInfo.xcprivacy (87%) rename packages/share_plus/share_plus/macos/{Classes => share_plus/Sources/share_plus}/SharePlusMacosPlugin.swift (100%) diff --git a/packages/share_plus/share_plus/.gitignore b/packages/share_plus/share_plus/.gitignore index 88ce490e47..cfd19d5e92 100644 --- a/packages/share_plus/share_plus/.gitignore +++ b/packages/share_plus/share_plus/.gitignore @@ -11,9 +11,11 @@ flutter_export_environment.sh examples/all_plugins/pubspec.yaml +.build/ Podfile Podfile.lock Pods/ +.swiftpm/ .symlinks/ **/Flutter/App.framework/ **/Flutter/ephemeral/ diff --git a/packages/share_plus/share_plus/ios/share_plus.podspec b/packages/share_plus/share_plus/ios/share_plus.podspec index 1d593aa8f1..e53d2d0774 100644 --- a/packages/share_plus/share_plus/ios/share_plus.podspec +++ b/packages/share_plus/share_plus/ios/share_plus.podspec @@ -14,13 +14,12 @@ Downloaded by pub (not CocoaPods). s.author = { 'Flutter Community Team' => 'authors@fluttercommunity.dev' } s.source = { :http => 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/share_plus/share_plus' } s.documentation_url = 'https://pub.dev/packages/share_plus' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.source_files = 'share_plus/Sources/share_plus/**/*.{h,m}' + s.public_header_files = 'share_plus/Sources/share_plus/include/**/*.h' s.dependency 'Flutter' s.ios.weak_framework = 'LinkPresentation' s.platform = :ios, '12.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - s.resource_bundles = {'share_plus_privacy' => ['PrivacyInfo.xcprivacy']} + s.resource_bundles = {'share_plus_privacy' => ['share_plus/Sources/share_plus/PrivacyInfo.xcprivacy']} end - diff --git a/packages/share_plus/share_plus/ios/share_plus/Package.swift b/packages/share_plus/share_plus/ios/share_plus/Package.swift new file mode 100644 index 0000000000..f13d48f6ab --- /dev/null +++ b/packages/share_plus/share_plus/ios/share_plus/Package.swift @@ -0,0 +1,27 @@ +// 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: "share_plus", + platforms: [ + .iOS("12.0"), + ], + products: [ + .library(name: "share-plus", targets: ["share_plus"]) + ], + dependencies: [], + targets: [ + .target( + name: "share_plus", + dependencies: [], + resources: [ + .process("PrivacyInfo.xcprivacy"), + ], + cSettings: [ + .headerSearchPath("include/share_plus") + ] + ) + ] +) diff --git a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m new file mode 100644 index 0000000000..c4cf236aba --- /dev/null +++ b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m @@ -0,0 +1,467 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#import "./include/share_plus/FPPSharePlusPlugin.h" +#import "LinkPresentation/LPLinkMetadata.h" +#import "LinkPresentation/LPMetadataProvider.h" + +static NSString *const PLATFORM_CHANNEL = @"dev.fluttercommunity.plus/share"; + +static UIViewController *RootViewController() { + if (@available(iOS 13, *)) { // UIApplication.keyWindow is deprecated + NSSet *scenes = [[UIApplication sharedApplication] connectedScenes]; + for (UIScene *scene in scenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + NSArray *windows = ((UIWindowScene *)scene).windows; + for (UIWindow *window in windows) { + if (window.isKeyWindow) { + return window.rootViewController; + } + } + } + } + return nil; + } else { + return [UIApplication sharedApplication].keyWindow.rootViewController; + } +} + +static UIViewController * +TopViewControllerForViewController(UIViewController *viewController) { + if (viewController.presentedViewController) { + return TopViewControllerForViewController( + viewController.presentedViewController); + } + if ([viewController isKindOfClass:[UINavigationController class]]) { + return TopViewControllerForViewController( + ((UINavigationController *)viewController).visibleViewController); + } + return viewController; +} + +// We need the companion to avoid ARC deadlock +@interface UIActivityViewSuccessCompanion : NSObject + +@property FlutterResult result; +@property NSString *activityType; +@property BOOL completed; + +- (id)initWithResult:(FlutterResult)result; + +@end + +@implementation UIActivityViewSuccessCompanion + +- (id)initWithResult:(FlutterResult)result { + if (self = [super init]) { + self.result = result; + self.completed = false; + } + return self; +} + +// We use dealloc as the share-sheet might disappear (e.g. iCloud photo album +// creation) and could then reappear if the user cancels +- (void)dealloc { + if (self.completed) { + self.result(self.activityType); + } else { + self.result(@""); + } +} + +@end + +@interface UIActivityViewSuccessController : UIActivityViewController + +@property UIActivityViewSuccessCompanion *companion; + +@end + +@implementation UIActivityViewSuccessController +@end + +@interface SharePlusData : NSObject + +@property(readonly, nonatomic, copy) NSString *subject; +@property(readonly, nonatomic, copy) NSString *text; +@property(readonly, nonatomic, copy) NSString *path; +@property(readonly, nonatomic, copy) NSString *mimeType; + +- (instancetype)initWithSubject:(NSString *)subject + text:(NSString *)text NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithFile:(NSString *)path + mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithFile:(NSString *)path + mimeType:(NSString *)mimeType + subject:(NSString *)subject NS_DESIGNATED_INITIALIZER; + +- (instancetype)init + __attribute__((unavailable("Use initWithSubject:text: instead"))); + +@end + +@implementation SharePlusData + +- (instancetype)init { + [super doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text { + self = [super init]; + if (self) { + _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject; + _text = text; + } + return self; +} + +- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType { + self = [super init]; + if (self) { + _path = path; + _mimeType = mimeType; + } + return self; +} + +- (instancetype)initWithFile:(NSString *)path + mimeType:(NSString *)mimeType + subject:(NSString *)subject { + self = [super init]; + if (self) { + _path = path; + _mimeType = mimeType; + _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject; + } + return self; +} + +- (id)activityViewControllerPlaceholderItem: + (UIActivityViewController *)activityViewController { + return [self + activityViewController:activityViewController + itemForActivityType:@"dev.fluttercommunity.share_plus.placeholder"]; +} + +- (id)activityViewController:(UIActivityViewController *)activityViewController + itemForActivityType:(UIActivityType)activityType { + if (!_path || !_mimeType) { + return _text; + } + + // If the shared file is an image return an UIImage for the placeholder + // to show a preview. + if ([activityType + isEqualToString:@"dev.fluttercommunity.share_plus.placeholder"] && + [_mimeType hasPrefix:@"image/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return image; + } + + // Return an NSURL for the real share to conserve the file name + NSURL *url = [NSURL fileURLWithPath:_path]; + return url; +} + +- (NSString *)activityViewController: + (UIActivityViewController *)activityViewController + subjectForActivityType:(UIActivityType)activityType { + return _subject; +} + +- (UIImage *)activityViewController: + (UIActivityViewController *)activityViewController + thumbnailImageForActivityType:(UIActivityType)activityType + suggestedSize:(CGSize)suggestedSize { + if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) { + return nil; + } + + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return [self imageWithImage:image scaledToSize:suggestedSize]; +} + +- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { + UIGraphicsBeginImageContext(newSize); + [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; +} + +- (LPLinkMetadata *)activityViewControllerLinkMetadata: + (UIActivityViewController *)activityViewController + API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0)) { + LPLinkMetadata *metadata = [[LPLinkMetadata alloc] init]; + + if ([_subject length] > 0) { + metadata.title = _subject; + } else if ([_text length] > 0) { + metadata.title = _text; + } + + if (_path) { + NSString *extesnion = [_path pathExtension]; + + unsigned long long rawSize = ( + [[[NSFileManager defaultManager] attributesOfItemAtPath:_path + error:nil] fileSize]); + NSString *readableSize = [NSByteCountFormatter + stringFromByteCount:rawSize + countStyle:NSByteCountFormatterCountStyleFile]; + + NSString *description = @""; + + if (![extesnion isEqualToString:@""]) { + description = + [description stringByAppendingString:[extesnion uppercaseString]]; + description = [description stringByAppendingString:@" • "]; + description = [description stringByAppendingString:readableSize]; + } else { + description = [description stringByAppendingString:readableSize]; + } + + // https://stackoverflow.com/questions/60563773/ios-13-share-sheet-changing-subtitle-item-description + metadata.originalURL = [NSURL fileURLWithPath:description]; + if (_mimeType && [_mimeType hasPrefix:@"image/"]) { + metadata.imageProvider = [[NSItemProvider alloc] + initWithObject:[UIImage imageWithContentsOfFile:_path]]; + } + } + + return metadata; +} + +@end + +@implementation FPPSharePlusPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *shareChannel = + [FlutterMethodChannel methodChannelWithName:PLATFORM_CHANNEL + binaryMessenger:registrar.messenger]; + + [shareChannel + setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { + NSDictionary *arguments = [call arguments]; + NSNumber *originX = arguments[@"originX"]; + NSNumber *originY = arguments[@"originY"]; + NSNumber *originWidth = arguments[@"originWidth"]; + NSNumber *originHeight = arguments[@"originHeight"]; + + CGRect originRect = CGRectZero; + if (originX && originY && originWidth && originHeight) { + originRect = + CGRectMake([originX doubleValue], [originY doubleValue], + [originWidth doubleValue], [originHeight doubleValue]); + } + + if ([@"share" isEqualToString:call.method]) { + NSString *shareText = arguments[@"text"]; + NSString *shareSubject = arguments[@"subject"]; + + if (shareText.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty text expected" + details:nil]); + return; + } + + UIViewController *rootViewController = RootViewController(); + if (!rootViewController) { + result([FlutterError errorWithCode:@"error" + message:@"No root view controller found" + details:nil]); + return; + } + UIViewController *topViewController = + TopViewControllerForViewController(rootViewController); + + [self shareText:shareText + subject:shareSubject + withController:topViewController + atSource:originRect + toResult:result]; + } else if ([@"shareFiles" isEqualToString:call.method]) { + NSArray *paths = arguments[@"paths"]; + NSArray *mimeTypes = arguments[@"mimeTypes"]; + NSString *subject = arguments[@"subject"]; + NSString *text = arguments[@"text"]; + + if (paths.count == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty paths expected" + details:nil]); + return; + } + + for (NSString *path in paths) { + if (path.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Each path must not be empty" + details:nil]); + return; + } + } + + UIViewController *rootViewController = RootViewController(); + if (!rootViewController) { + result([FlutterError errorWithCode:@"error" + message:@"No root view controller found" + details:nil]); + return; + } + UIViewController *topViewController = + TopViewControllerForViewController(rootViewController); + [self shareFiles:paths + withMimeType:mimeTypes + withSubject:subject + withText:text + withController:topViewController + atSource:originRect + toResult:result]; + } else if ([@"shareUri" isEqualToString:call.method]) { + NSString *uri = arguments[@"uri"]; + + if (uri.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty uri expected" + details:nil]); + return; + } + + UIViewController *rootViewController = RootViewController(); + if (!rootViewController) { + result([FlutterError errorWithCode:@"error" + message:@"No root view controller found" + details:nil]); + return; + } + UIViewController *topViewController = + TopViewControllerForViewController(rootViewController); + + [self shareUri:uri + withController:topViewController + atSource:originRect + toResult:result]; + } else { + result(FlutterMethodNotImplemented); + } + }]; +} + ++ (void)share:(NSArray *)shareItems + withSubject:(NSString *)subject + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { + UIActivityViewSuccessController *activityViewController = + [[UIActivityViewSuccessController alloc] initWithActivityItems:shareItems + applicationActivities:nil]; + + // Force subject when sharing a raw url or files + if (![subject isKindOfClass:[NSNull class]]) { + [activityViewController setValue:subject forKey:@"subject"]; + } + + activityViewController.popoverPresentationController.sourceView = + controller.view; + BOOL isCoordinateSpaceOfSourceView = + CGRectContainsRect(controller.view.frame, origin); + + // If device is e.g. an iPad then hasPopoverPresentationController is true + BOOL hasPopoverPresentationController = + [activityViewController popoverPresentationController] != NULL; + if (hasPopoverPresentationController && + (!isCoordinateSpaceOfSourceView || CGRectIsEmpty(origin))) { + NSString *sharePositionIssue = [NSString + stringWithFormat: + @"sharePositionOrigin: argument must be set, %@ must be non-zero " + @"and within coordinate space of source view: %@", + NSStringFromCGRect(origin), + NSStringFromCGRect(controller.view.bounds)]; + + result([FlutterError errorWithCode:@"error" + message:sharePositionIssue + details:nil]); + return; + } + + if (!CGRectIsEmpty(origin)) { + activityViewController.popoverPresentationController.sourceRect = origin; + } + + UIActivityViewSuccessCompanion *companion = + [[UIActivityViewSuccessCompanion alloc] initWithResult:result]; + activityViewController.companion = companion; + activityViewController.completionWithItemsHandler = + ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, + NSError *activityError) { + companion.activityType = activityType; + companion.completed = completed; + }; + + [controller presentViewController:activityViewController + animated:YES + completion:nil]; +} + ++ (void)shareUri:(NSString *)uri + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { + NSURL *data = [NSURL URLWithString:uri]; + [self share:@[ data ] + withSubject:nil + withController:controller + atSource:origin + toResult:result]; +} + ++ (void)shareText:(NSString *)shareText + subject:(NSString *)subject + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { + NSObject *data = [[SharePlusData alloc] initWithSubject:subject + text:shareText]; + [self share:@[ data ] + withSubject:subject + withController:controller + atSource:origin + toResult:result]; +} + ++ (void)shareFiles:(NSArray *)paths + withMimeType:(NSArray *)mimeTypes + withSubject:(NSString *)subject + withText:(NSString *)text + withController:(UIViewController *)controller + atSource:(CGRect)origin + toResult:(FlutterResult)result { + NSMutableArray *items = [[NSMutableArray alloc] init]; + + for (int i = 0; i < [paths count]; i++) { + NSString *path = paths[i]; + NSString *mimeType = mimeTypes[i]; + [items addObject:[[SharePlusData alloc] initWithFile:path + mimeType:mimeType + subject:subject]]; + } + if (text != nil) { + NSObject *data = [[SharePlusData alloc] initWithSubject:subject text:text]; + [items addObject:data]; + } + + [self share:items + withSubject:subject + withController:controller + atSource:origin + toResult:result]; +} + +@end diff --git a/packages/share_plus/share_plus/macos/PrivacyInfo.xcprivacy b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/PrivacyInfo.xcprivacy similarity index 100% rename from packages/share_plus/share_plus/macos/PrivacyInfo.xcprivacy rename to packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/PrivacyInfo.xcprivacy diff --git a/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/include/share_plus/FPPSharePlusPlugin.h b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/include/share_plus/FPPSharePlusPlugin.h new file mode 100644 index 0000000000..d9b647ec08 --- /dev/null +++ b/packages/share_plus/share_plus/ios/share_plus/Sources/share_plus/include/share_plus/FPPSharePlusPlugin.h @@ -0,0 +1,8 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +@interface FPPSharePlusPlugin : NSObject +@end diff --git a/packages/share_plus/share_plus/macos/share_plus.podspec b/packages/share_plus/share_plus/macos/share_plus.podspec index c0991cd138..344b797c67 100644 --- a/packages/share_plus/share_plus/macos/share_plus.podspec +++ b/packages/share_plus/share_plus/macos/share_plus.podspec @@ -13,8 +13,8 @@ https://github.com/flutter/flutter/issues/46618 s.license = { :file => '../LICENSE' } s.author = { 'Flutter Community' => 'authors@fluttercommunity.dev' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' + s.source_files = 'share_plus/Sources/share_plus/**/*.swift' + s.public_header_files = 'share_plus/Sources/share_plus/**/*.h' s.dependency 'FlutterMacOS' s.platform = :osx diff --git a/packages/share_plus/share_plus/macos/share_plus/Package.swift b/packages/share_plus/share_plus/macos/share_plus/Package.swift new file mode 100644 index 0000000000..c5c07d2624 --- /dev/null +++ b/packages/share_plus/share_plus/macos/share_plus/Package.swift @@ -0,0 +1,24 @@ +// 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: "share_plus", + platforms: [ + .macOS("10.14") + ], + products: [ + .library(name: "share-plus", targets: ["share_plus"]) + ], + dependencies: [], + targets: [ + .target( + name: "share_plus", + dependencies: [], + resources: [ + .process("PrivacyInfo.xcprivacy") + ] + ) + ] +) diff --git a/packages/share_plus/share_plus/ios/PrivacyInfo.xcprivacy b/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/PrivacyInfo.xcprivacy similarity index 87% rename from packages/share_plus/share_plus/ios/PrivacyInfo.xcprivacy rename to packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/PrivacyInfo.xcprivacy index a34b7e2e60..918d80be43 100644 --- a/packages/share_plus/share_plus/ios/PrivacyInfo.xcprivacy +++ b/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/PrivacyInfo.xcprivacy @@ -4,8 +4,6 @@ NSPrivacyTrackingDomains - NSPrivacyAccessedAPITypes - NSPrivacyCollectedDataTypes NSPrivacyTracking diff --git a/packages/share_plus/share_plus/macos/Classes/SharePlusMacosPlugin.swift b/packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift similarity index 100% rename from packages/share_plus/share_plus/macos/Classes/SharePlusMacosPlugin.swift rename to packages/share_plus/share_plus/macos/share_plus/Sources/share_plus/SharePlusMacosPlugin.swift