从 SwiftUI 视图呈现 UIActivityViewController

Posted

技术标签:

【中文标题】从 SwiftUI 视图呈现 UIActivityViewController【英文标题】:Presenting UIActivityViewController from SwiftUI View 【发布时间】:2019-10-08 12:26:03 【问题描述】:

我正在尝试从 SwiftUI 视图中呈现 UIActivityViewController(共享表)。我创建了一个名为ShareSheet 的视图符合UIViewControllerRepresentable 来配置UIActivityViewController,但实际呈现它并不是那么简单。

struct ShareSheet: UIViewControllerRepresentable 
    typealias UIViewControllerType = UIActivityViewController

    var sharing: [Any]

    func makeUIViewController(context: UIViewControllerRepresentableContext<ShareSheet>) -> UIActivityViewController 
        UIActivityViewController(activityItems: sharing, applicationActivities: nil)
    

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

    

通过.sheet 天真地这样做会导致以下结果。

.sheet(isPresented: $showShareSheet) 
    ShareSheet(sharing: [URL(string: "https://example.com")!])

有没有办法像通常呈现的那样呈现这个?就像覆盖半个屏幕一样?

【问题讨论】:

【参考方案1】:

希望对你有帮助,

struct ShareSheetView: View 
    var body: some View 
        Button(action: actionSheet) 
            Image(systemName: "square.and.arrow.up")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 36, height: 36)
        
    
    
    func actionSheet() 
        guard let data = URL(string: "https://www.apple.com") else  return 
        let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
        UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
    

【讨论】:

抱歉回复晚了,谢谢! 这是一个糟糕的解决方案,不应标记为已接受。 ` UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)` 在很多场景下会无法获取到正确的vc。 我在尝试使用 windows first.present 时看到了这个问题。我根据 OP 的版本使用了 .sheet 。需要注意的是共享表中的键盘无法正常工作。如果您的工作表不包含在父视图中,则响应者链很脆弱,我转而使用最外层的父视图。看起来像是 ios15 自带的东西。【参考方案2】:

至少在 iOS 14、Swift 5、Xcode 12.5 中,我只需将 UIActivityViewController 包装在另一个视图控制器中即可相当轻松地完成此操作。它不需要检查视图层次结构或使用任何 3rd 方库。唯一骇人听闻的部分是异步呈现视图控制器,这甚至可能不是必需的。有更多 SwiftUI 经验的人或许可以提供改进建议。

import Foundation
import SwiftUI
import UIKit

struct ActivityViewController: UIViewControllerRepresentable 
        
    @Binding var shareURL: URL?
    
    func makeCoordinator() -> Coordinator 
        Coordinator(self)
    
    
    func makeUIViewController(context: Context) -> some UIViewController 
        let containerViewController = UIViewController()
        
        return containerViewController

    
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) 
        guard let shareURL = shareURL, context.coordinator.presented == false else  return 
        
        context.coordinator.presented = true

        let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil)
        activityViewController.completionWithItemsHandler =  activity, completed, returnedItems, activityError in
            self.shareURL = nil
            context.coordinator.presented = false

            if completed 
                // ...
             else 
                // ...
            
        
        
        // Executing this asynchronously might not be necessary but some of my tests
        // failed because the view wasn't yet in the view hierarchy on the first pass of updateUIViewController
        //
        // There might be a better way to test for that condition in the guard statement and execute this
        // synchronously if we can be be sure updateUIViewController is invoked at least once after the view is added
        DispatchQueue.main.asyncAfter(deadline: .now()) 
            uiViewController.present(activityViewController, animated: true)
        
    
    
    class Coordinator: NSObject 
        let parent: ActivityViewController
        
        var presented: Bool = false
        
        init(_ parent: ActivityViewController) 
            self.parent = parent
        
    
    

struct ContentView: View 
    
    @State var shareURL: URL? = nil
    
    var body: some View 
        ZStack 
            Button(action:  shareURL = URL(string: "https://apple.com") ) 
                Text("Share")
                    .foregroundColor(.white)
                    .padding()
            
            .background(Color.blue)
            if shareURL != nil 
                ActivityViewController(shareURL: $shareURL)
            
        
        .frame(width: 375, height: 812)
    

【讨论】:

【参考方案3】:

有一个UIModalPresentationStyle 可用于显示某些演示文稿:

case pageSheet

一种部分覆盖底层内容的展示风格。

你应用演示风格的方式:

func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController 
    let v = UIActivityViewController(activityItems: sharing, applicationActivities: nil)
    v.modalPresentationStyle = .pageSheet
    return v

可在此处找到演示文稿列表:

https://developer.apple.com/documentation/uikit/uimodalpresentationstyle

我还没有亲自测试过它们,所以如果最终没有像您预期的那样工作,我提前道歉。

或者,您可以查看this answer,他们在其中提到了第三方库,这将允许您以通常呈现的方式创建半模态。

【讨论】:

感谢您的提示,但我不确定是否可以使用 SwiftUI 指定 UIModalPresentationStyle :/ @KilianKoeltzsch 我已经更新了答案,包括您如何应用modalPresentationStyle。说您最好修改第三方库以满足您的需求。您也将拥有更多控制权。 啊,有意思,我在创建视图控制器的时候都没想过要设置它。虽然通过.sheet() 显示它时似乎没有得到尊重。我也会研究链接选项。 即使您将 SwiftUI 视图包装在 UIHostingController 中并将包装器的 modalPresentationStyle 设置为 pageSheet,然后使用顶部控制器呈现上述内容,仍然几乎是全屏的 (就像问题中的图片一样)。 pageSheet 当然受到尊重,因为设置为其他内容确实会改变外观。唉,我无法让纸张半覆盖屏幕。【参考方案4】:

iOS 15 / Swift 5 / Xcode 13

获取顶部呈现的 UIViewController 的扩展:

import UIKit

extension UIApplication 
    
    // MARK: No shame!
    
    static func TopPresentedViewController() -> UIViewController? 
        
        guard let rootViewController = UIApplication.shared
                .connectedScenes.lazy
                .compactMap( $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil )
                .first(where:  $0.keyWindow != nil )?
                .keyWindow?
                .rootViewController
        else 
            return nil
        
        
        var topController = rootViewController
        
        while let presentedViewController = topController.presentedViewController 
            topController = presentedViewController
        
        
        return topController
        
    
    

然后用它来展示你的 UIActivityViewController:

UIApplication.TopPresentedViewController?.present(activityViewController, animated: true, completion: nil)

原始答案(已弃用的代码):

它并不漂亮,但你可以直接这样调用它(考虑到你的应用只有 1 个窗口):

UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true, completion: nil)

如果你得到一些警告blablabla:

警告:尝试展示...已经在展示...

您可以执行this to get the top most view controller 之类的操作并调用present。

【讨论】:

以上是关于从 SwiftUI 视图呈现 UIActivityViewController的主要内容,如果未能解决你的问题,请参考以下文章

从 SwiftUI 视图呈现 UIActivityViewController

从公共 API 获取 JSON 以在 SwiftUI 的列表视图中呈现

如何将关闭按钮从 swiftui 视图的主结构(由 uihostingviewcontroller 呈现)分离到它自己的结构?

从 NavigationView 呈现的 SwiftUI 关闭模式表(Xcode Beta 5)

如何在基于 SwiftUI 的 iOS 应用中呈现连续视图?

在 SwiftUI 中从 UIViewRepresentable 呈现视图