在SwiftUI中展示“UIActivityViewController”

77

我希望让用户能够分享位置,但我不知道如何在SwiftUI中显示UIActivityViewController

15个回答

87

SwiftUIUIActivityViewController 的基本实现为:

import UIKit
import SwiftUI

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}

}

这是如何使用它的方法。

struct MyView: View {

    @State private var isSharePresented: Bool = false

    var body: some View {
        Button("Share app") {
            self.isSharePresented = true
        }
        .sheet(isPresented: $isSharePresented, onDismiss: {
            print("Dismiss")
        }, content: {
            ActivityViewController(activityItems: [URL(string: "https://www.apple.com")!])
        })
    }
}

我有图片链接要分享,那种情况下应该怎么办?在使用 UIImage(named:"Product") 时有效,但在 SwiftUI 中则使用 Image("Product")。为了在视图中显示图片,我使用 ImageViewContainer1(imageUrl: self.item.image!.url),但它没有报错。 - Mario Burga
如果有人愿意分享应用程序链接,URL 为“apps.apple.com/us/app/id(您的应用程序 ID)”。 - Ahmadreza
13
由于未设置popoverPresentationController,这将在iPad上崩溃。 - Caleb Friden
2
@CalebFriden,你能提供一些有关崩溃的详细信息吗?我在模拟器和物理设备(iPad Pro 11" 2018)上尝试了iPadOS 14.2,但它没有崩溃。 - Darrarski
2
我刚在iPad模拟器上尝试了一下,它完美地运行。 - sabiland

24

基于Tikhonov的方法,以下代码添加了一个修复程序以确保活动表单正确关闭(否则后续表单将无法呈现)。

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    @Environment(\.presentationMode) var presentationMode

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
            self.presentationMode.wrappedValue.dismiss()
        }
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}

}

由于presentationMode已被弃用,您可以使用@Environment(\.dismiss) private var dismissAction,然后调用self.dismissAction() - undefined

16

目前这是一次性的事情。.sheet将其显示为表格,但从同一视图再次打开它将具有陈旧的数据。这些后续的表格显示也不会触发任何完成处理程序。基本上,makeUIViewController仅被调用一次,这是将数据共享到UIActivityViewController的唯一方法。updateUIViewController无法更新activityItems中的数据或重置控制器,因为这些在UIActivityViewController的实例中不可见。

请注意,它也不适用于UIActivityItemSource或UIActivityItemProvider。使用这些甚至更糟。占位符值不会显示。

我进行了更多的黑客工作,并决定我的解决方案的问题可能是一个呈现另一个表格的表格,当一个消失时,另一个保持不变。

通过这种间接的方式,让ViewController在出现时进行演示,这对我起作用了。

class UIActivityViewControllerHost: UIViewController {
    var message = ""
    var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? = nil
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        share()
    }
    
    func share() {
        // set up activity view controller
        let textToShare = [ message ]
        let activityViewController = UIActivityViewController(activityItems: textToShare, applicationActivities: nil)

        activityViewController.completionWithItemsHandler = completionWithItemsHandler
        activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash

        // present the view controller
        self.present(activityViewController, animated: true, completion: nil)
    }
}

struct ActivityViewController: UIViewControllerRepresentable {
    @Binding var text: String
    @Binding var showing: Bool
    
    func makeUIViewController(context: Context) -> UIActivityViewControllerHost {
        // Create the host and setup the conditions for destroying it
        let result = UIActivityViewControllerHost()
        
        result.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
            // To indicate to the hosting view this should be "dismissed"
            self.showing = false
        }
        
        return result
    }
    
    func updateUIViewController(_ uiViewController: UIActivityViewControllerHost, context: Context) {
        // Update the text in the hosting controller
        uiViewController.message = text
    }
    
}

struct ContentView: View {
    @State private var showSheet = false
    @State private var message = "a message"
    
    var body: some View {
        VStack {
            TextField("what to share", text: $message)
            
            Button("Hello World") {
                self.showSheet = true
            }
            
            if showSheet {
                ActivityViewController(text: $message, showing: $showSheet)
                    .frame(width: 0, height: 0)
            }
            
            Spacer()
        }
        .padding()
    }
}

6
viewDidAppear必须调用super。 - Mycroft Canner

14
也许这并不被推荐,但是通过两行代码(适用于iPhone)就能轻松地分享文本。
Button(action: {
      let shareActivity = UIActivityViewController(activityItems: ["Text To Share"], applicationActivities: nil)
      if let vc = UIApplication.shared.windows.first?.rootViewController{
          shareActivity.popoverPresentationController?.sourceView = vc.view
         //Setup share activity position on screen on bottom center
          shareActivity.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height, width: 0, height: 0)
          shareActivity.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.down
         vc.present(shareActivity, animated: true, completion: nil)
      }
}) {
    Text("Share")
}

编辑:现在在iPad上运行良好(在iPad Pro(9.7英寸)模拟器上测试过)

iOS 15

        Button(action: {
            guard let vc = UIApplication.shared.connectedScenes.compactMap({$0 as? UIWindowScene}).first?.windows.first?.rootViewController else{
                return
            }
            let shareActivity = UIActivityViewController(activityItems: ["Text To Share"], applicationActivities: nil)
            shareActivity.popoverPresentationController?.sourceView = vc.view
            shareActivity.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height, width: 0, height: 0)
            shareActivity.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.down
            vc.present(shareActivity, animated: true, completion: nil)
        }) {
            Text("Share")
        }

这在iPhone上可以运行,但在iPad上会崩溃,你有什么办法让它也能在iPad上运行吗? - Ali
2
@Ali,它崩溃了,因为在iPad上它不知道sourceView在哪里。https://dev59.com/6V8e5IYBdhLWcg3wjaxI#65785434 - LetsGoBrandon
在iOS 15.0中,编译器警告UIApplication.shared.windows已被弃用:请改用相关窗口场景上的UIWindowScene.windows。能否请您更新此答案? - undefined

8

我希望提出另一种看起来更本地化的实现方式(半屏高度,底部没有白色间隙)。

import SwiftUI

struct ActivityView: UIViewControllerRepresentable {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]? = nil
    @Binding var isPresented: Bool

    func makeUIViewController(context: Context) -> ActivityViewWrapper {
        ActivityViewWrapper(activityItems: activityItems, applicationActivities: applicationActivities, isPresented: $isPresented)
    }

    func updateUIViewController(_ uiViewController: ActivityViewWrapper, context: Context) {
        uiViewController.isPresented = $isPresented
        uiViewController.updateState()
    }
}

class ActivityViewWrapper: UIViewController {
    var activityItems: [Any]
    var applicationActivities: [UIActivity]?

    var isPresented: Binding<Bool>

    init(activityItems: [Any], applicationActivities: [UIActivity]? = nil, isPresented: Binding<Bool>) {
        self.activityItems = activityItems
        self.applicationActivities = applicationActivities
        self.isPresented = isPresented
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        updateState()
    }

    fileprivate func updateState() {
        guard parent != nil else {return}
        let isActivityPresented = presentedViewController != nil
        if isActivityPresented != isPresented.wrappedValue {
            if !isActivityPresented {
                let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
                controller.completionWithItemsHandler = { (activityType, completed, _, _) in
                    self.isPresented.wrappedValue = false
                }
                present(controller, animated: true, completion: nil)
            }
            else {
                self.presentedViewController?.dismiss(animated: true, completion: nil)
            }
        }
    }
}

struct ActivityViewTest: View {
    @State private var isActivityPresented = false
    var body: some View {
        Button("Preset") {
            self.isActivityPresented = true
        }.background(ActivityView(activityItems: ["Hello, World"], isPresented: $isActivityPresented))
    }
}

struct ActivityView_Previews: PreviewProvider {
    static var previews: some View {
        ActivityViewTest()
    }
}

这在iPad上崩溃了。 - Charlie Fish
这在iPad上崩溃了。 - undefined
以防万一有人来到这里。这个解决方案在iPad上运行得非常完美,但是它有一个问题,当呈现时可能会导致同一视图上附加的其他表单自动关闭。这个修改修复了这个问题: "let isActivityPresented = presentedViewController as? UIActivityViewController != nil" - undefined

6
如果您需要更细粒度地控制在共享表中显示的内容,则可能需要实现 UIActivityItemSource。我尝试使用上面Mike W.的代码,但起初它没有起作用(代理函数未被调用)。解决方法是在 makeUIViewController 中更改 UIActivityController 的初始化方式,如下所示,现在将[context.coordinator] 作为 activityItems 传递:
let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)

此外,我想在分享面板中设置图标、标题和副标题,因此我已经在Coordinator类中实现了activityViewControllerLinkMetadata函数。

以下是Mike W.回答的完整扩展版本。请注意,您需要将import LinkPresentation添加到代码中。

ActivityViewController

import SwiftUI
import LinkPresentation

struct ActivityViewController: UIViewControllerRepresentable {
    var shareable : ActivityShareable?
    var applicationActivities: [UIActivity]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)
        controller.modalPresentationStyle = .automatic
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}

    func makeCoordinator() -> ActivityViewController.Coordinator {
        Coordinator(self.shareable)
    }

    class Coordinator : NSObject, UIActivityItemSource {
        private let shareable : ActivityShareable?

        init(_ shareable: ActivityShareable?) {
            self.shareable = shareable
            super.init()
        }

        func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
            guard let share = self.shareable else { return "" }
            return share.getPlaceholderItem()
        }

        func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
            guard let share = self.shareable else { return "" }
            return share.itemForActivityType(activityType: activityType)
        }

        func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
            guard let share = self.shareable else { return "" }
            return share.subjectForActivityType(activityType: activityType)
        }
        
        func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
            guard let share = self.shareable else { return nil }
            
            let metadata = LPLinkMetadata()

            // share sheet preview title
            metadata.title = share.shareSheetTitle()
            // share sheet preview subtitle
            metadata.originalURL = URL(fileURLWithPath: share.shareSheetSubTitle())
            // share sheet preview icon
            if let image = share.shareSheetIcon() {
                let imageProvider = NSItemProvider(object: image)
                metadata.imageProvider = imageProvider
                metadata.iconrovider = imageProvider
            }
            return metadata
        }
    }
}   

协议ActivityShareable

protocol ActivityShareable {
    func getPlaceholderItem() -> Any
    func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?
    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
    func shareSheetTitle() -> String
    func shareSheetSubTitle() -> String
    func shareSheetIcon() -> UIImage?
}

在我的情况下,我正在使用分享表单来导出文本,因此我创建了一个称为ActivityShareableText的结构体,它符合ActivityShareable接口。
struct ActivityShareableText: ActivityShareable {
    let text: String
    let title: String
    let subTitle: String
    let icon: UIImage?
    
    func getPlaceholderItem() -> Any {
        return text
    }
    
    func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any? {
        return text
    }
    
    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
        return "\(title): \(subTitle)"
    }
    
    func shareSheetTitle() -> String {
        return title
    }
    
    func shareSheetSubTitle() -> String {
        return subTitle
    }
    
    func shareSheetIcon() -> UIImage? {
        return icon
    }
}

我的代码中,我按照以下方式调用分享面板:

ActivityViewController(shareable: ActivityShareableText(
    text: myShareText(),
    title: myShareTitle(),
    subTitle: myShareSubTitle(),
    icon: UIImage(named: "myAppLogo")
))

这是目前为止最好、最健壮的答案。 - JAHelia
有没有一种方法可以为主题设置 AttributedString - JAHelia
太棒了!你知道是否可以扩大标题间距吗?我的标题被截断了。 - lopes710

5

我现在已经成功使用了以下代码:

.sheet(isPresented: $isSheet, content: { ActivityViewController() })

注意,.presentation已被弃用。

此代码将占据整个屏幕,iOS 13风格。


你的意思是在闭包中使用 UIActivityViewController() 吗? - Bill
@Bill 不,这种情况下ActivityViewController是一个 SwiftUI 视图,用于包装普通的 UIActivityViewController - AverageHelper
2
也许需要补充说明这是一个自定义包装器。 - Teng L

4

在@Shimanski Artem的解决方案基础上进行扩展。我认为我们可以更简洁地编写代码。因此,我将我的ActivityViewController嵌入到一个空白的UIViewController中,并从那里呈现它。这样我们不会获得完整的“覆盖层”表格,而且您还可以获得本机行为。就像@Shimanski Artem所做的那样。

struct UIKitActivityView: UIViewControllerRepresentable {
    @Binding var isPresented: Bool

    let data: [Any]

    func makeUIViewController(context: Context) -> UIViewController {
        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        let activityViewController = UIActivityViewController(
            activityItems: data,
            applicationActivities: nil
        )

        if isPresented && uiViewController.presentedViewController == nil {
            uiViewController.present(activityViewController, animated: true)
        }

        activityViewController.completionWithItemsHandler = { (_, _, _, _) in
            isPresented = false
        }
    }
}

用法

struct ActivityViewTest: View {
    @State private var isActivityPresented = false

    var body: some View {
        Button("Preset") {
           self.isActivityPresented = true
        }
        .background(
            UIKitActivityView(
                isPresented: $viewModel.showShareSheet,
                data: ["String"]
            )
        )
    }
}

1
你的代码在iPad上无法运行,只能在iPhone上运行,因为没有提供sourceView。 - Daniel

4

FWIW - 这是一个提供了对 UIActivityItemSource 实现稍微改进的答案。代码已经简化,尤其是在 itemForActivityTypeactivityViewControllerPlaceholderItem 上的默认返回值,它们必须始终返回相同的类型。

ActivityViewController

struct ActivityViewController: UIViewControllerRepresentable {

    var activityItems: [Any]
    var shareable : ActivityShareable?
    var applicationActivities: [UIActivity]? = nil

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        controller.modalPresentationStyle = .automatic
        return controller
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}

    func makeCoordinator() -> ActivityViewController.Coordinator {
        Coordinator(self.shareable)
    }

    class Coordinator : NSObject, UIActivityItemSource {

        private let shareable : ActivityShareable?

        init(_ shareable: ActivityShareable?) {
            self.shareable = shareable
            super.init()
        }

        func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
            guard let share = self.shareable else { return "" }
            return share.getPlaceholderItem()
        }

        func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
            guard let share = self.shareable else { return "" }
            return share.itemForActivityType(activityType: activityType)
        }

        func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
            guard let share = self.shareable else { return "" }
            return share.subjectForActivityType(activityType: activityType)
        }
    }
}

ActivityShareable

protocol ActivityShareable {

    func getPlaceholderItem() -> Any
    func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?

    /// Optional
    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
}

extension ActivityShareable {

    func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
        return ""
    }
}

你可以传入 ActivityViewController 的引用或底层的 UIActivityViewController,但感觉这样做有些多余。

3

您可以尝试将 UIActivityViewController 移植到 SwiftUI,方法如下:

struct ActivityView: UIViewControllerRepresentable {

    let activityItems: [Any]
    let applicationActivities: [UIActivity]?

    func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
        return UIActivityViewController(activityItems: activityItems,
                                        applicationActivities: applicationActivities)
    }

    func updateUIViewController(_ uiViewController: UIActivityViewController,
                                context: UIViewControllerRepresentableContext<ActivityView>) {

    }
}

但是当你尝试显示它时,应用程序会崩溃。

我尝试过:ModalPopoverNavigationButton

测试方法:

struct ContentView: View {
    var body: some Body {
        EmptyView
        .presentation(Modal(ActivityView()))
    }
}

它似乎无法从SwiftUI中使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接